General
El examen se presentará el 16 de diciembre a las 20:00 en Moodle.
El examen se presentará el 16 de diciembre a las 20:00 en Moodle.
En este curso vamos a estudiar los aspectos teóricos de los sistemas distribuidos y cómo la distribución del cómputo y los datos tienen muchas ventajas sobre los sistemas centralizados.
Vamos utilizar el cómputo en la nube
Actualmente todos usamos algún servicio en la nube. Por ejemplo Hotmail, Gmail, Youtube, Uber, Netflix y Office 365 son ejemplos de servicios en la nube. También Google Drive y OneDrive son servicios en la nube.
Los primeros son aplicaciones (distribuidas) que ejecutan en la nube y los segundos son servicios de almacenamiento en la nube.
Las empresas están migrando sus sistemas a la nube, por esa razón es muy importante que los egresados de la ESCOM puedan desarrollar, instalar y/o administrar sistemas en la nube.
En nuestro curso vamos a utilizar la nube de Microsoft llamada Azure. Para ello es necesario que todos tengan una cuenta de correo institucional del IPN y se inscriban al programa gratuito Azure for Students.
Este programa, Microsoft les regala 100 dólares en servicios de nube de Azure durante un año, sin la necesidad de dar una tarjeta de crédito. Sólo es necesario demostrar su condición de alumno (mediante la cuenta de correo institucional).
Inicialmente vamos a explicar cómo crear máquinas virtuales en la nube (Linux y Windows).
Entonces vamos a utilizar las máquinas virtuales como una red de computadoras dónde probaremos los sistemas distribuidos que desarrollaremos durante el curso.
Debido a que 100 dólares no es mucho es términos de servicios en la nube, deberemos tener mucho cuidado en apagar o eliminar las máquinas virtuales tan pronto realicemos alguna prueba o tarea.
Vamos a jugar
En nuestro curso vamos a implementar la "gamificación" (game=juego) como apoyo didáctico.
Vamos a jugar kahoots sobre los temas del curso. A los ganadores de cada kahoot se les otorgará puntos directos a la calificación parcial; 1/4 de punto al primer lugar, 1/6 de punto al segundo lugar y 1/8 de punto al tercer lugar.
Se agregará a la calificación del parcial, los puntos de kahoots que cada alumno ganó en el mismo parcial. Cada alumno solo podrá aplicar un máximo de 1 punto extra por kahoots cada parcial.
Jugar los kahoots será opcional, pero es conveniente que todos jueguen ya que los exámenes parciales podrían incluir preguntas parecidas a las de los kahoots.
Si se sobrepasa la calificación de 10 después de agregar los puntos de kahoot, la calificación que se asentará en el parcial será 10, el excedente no se aplicará a los siguientes parciales. Los puntos de los kahoots no son transferibles a los siguientes parciales.
Es necesario que los alumnos accedan a cada kahoot con su nombre y apellidos, por ejemplo JuanLopezMorales, de manera que sea posible identificar a los ganadores de puntos extra.
Evaluación parcial
Cada parcial se evaluará de la siguiente forma:
Las tareas se deberán entregar en tiempo y forma en la plataforma moodle. No habrá extensión en la fecha de entrega de las tareas, salvo causas plenamente justificadas.
Se recomienda realizar las tareas tan pronto se publiquen en moodle, de tal manera que si tienen alguna duda o de plano no corre el programa, puedan consultar con el profesor.
Como pueden ver, las tareas tienen la mayor ponderación en la calificación.
Asistencia a clases
Las clases se van a impartir por videoconferencia. Para acceder a las clases se deberá utilizar el enlace “Acceso a la clase” disponible en la sección “Avisos” de la plataforma.
Deberán acceder a la sesión de videoconferencia con su nombre completo, de manera que sea posible identificarlos y darles acceso a la sesión.
Podrán ingresar a la sesión de videoconferencia en cualquier momento dentro del horario de clase. Se pasará lista de asistencia en la sesión de videoconferencia. La tolerancia para tener asistencia será de 15 minutos.
Se solicitará a los alumnos que presenten su pantalla en la sesión de videoconferencia para revisar el avance en la realización de sus actividades. Para tener asistencia en clase, los alumnos deberán realizar las actividades de la clase.
Los alumnos obtendrán el 10% de participación en clase si tiene al menos el 80% de asistencias en el parcial.
Para poder presentar el examen parcial los alumnos y alumnas deberán tener al menos el 80% de asistencias en el parcial.
Broadcast. El broadcast es un tipo de multi-transmisión en la cual una computadora envía mensajes a todas las computadoras en una red.
Multicast. El multicast también es un tipo de multi-transmisión en la cual una computadora envía mensajes a una o más computadoras en una red. El broadcast es un caso particular de multicast, cuando la computadora envía mensajes a todas las computadoras de la red.
Una dirección IP versión 4 es un número de 32 bits dividido en cuatro bytes, cada byte puede tener un valor entre 0 y 255.
El puerto es un número entre 0 y 65,535. Los puertos del 0 a 1023 están reservados.
Un socket es un punto final (endpoint) de un enlace de dos vías que comunica dos procesos que ejecutan en la red. Un endpoint es la combinación de una dirección IP y un puerto.Clases de dirección IP v4
Las direcciones IP versión 4 se dividen en cinco clases o rangos, a saber: Clase A, Clase B, Clase C, Clase D y Clase E. Cada clase se define por un rango de valores que puede tomar el primer byte de la dirección IP, así las clases A, B y C son utilizadas para la comunicación unicast, mientras que la clase D es utilizada exclusivamente para la comunicación multicast. La clase E está reservada para propósitos experimentales.
La siguiente tabla muestra los bytes que identifican a las redes y a los hosts en cada clase (Rango del primer byte), así como la máscara de subred, número de redes y número de hosts por red en cada clase.
Las direcciones 127.X.X.X (loopback address) son utilizadas para identificar a la computadora local (localhost).
La dirección 255.255.255.255 es usada para broadcast a todos los hosts en la LAN. Las direcciones 224.0.0.1 y 224.0.0.255 están reservadas.
La clase D a su vez se divide en tres rangos de acuerdo a su uso:
Fuente: http://www.tcpipguide.com/free/t_IPMulticastAddressing.htm
Socket stream (socket orientado a conexión)
Socket datagrama (socket sin conexión)
La clase de hoy vamos a ver cómo programar un cliente y un servidor en Java.
Un cliente es un programa que se conecta a un programa servidor. Notar que el cliente inicia la conexión con el servidor.
Una vez que el cliente está conectado al servidor, el cliente puede enviar datos al servidor y el servidor puede mandar datos al cliente. A este tipo de comunicación se le conoce como bi-direccional, debido a que los datos pueden fluir en ambas direcciones.
En particular, los clientes y servidores que utilizaremos en el curso usan sockets TCP.
Para compilar y ejecutar los programas del curso vamos a utilizar JDK8 desde la línea de comandos.
Los que quieran utilizar ambientes de desarrollo como Netbeans o Eclipse pueden hacerlo, sin embargo en general vamos a ejecutar los programas en la línea de comandos.
Cliente.java
El programa Cliente.java es un ejemplo de un cliente de sockets TCP que se conecta a un servidor y posteriormente envía y recibe datos.
Primeramente vamos a crear un socket que se conectará al servidor. En este caso el servidor se llama "localhost" (computadora local) y el puerto abierto en el servidor es el 50000. En general el nombre del servidor puede ser un nombre de dominio (como midominio.com o una dirección IP). El número de puerto es un número entero entre 0 y 65535.Socket conexion = new Socket("localhost",50000);En este caso declaramos una variable de tipo Socket llamada "conexión" la cual va a contener una instancia de la clase Socket.Es importante aclarar que antes de crear el socket, el programa servidor debe estar en ejecución y esperando una conexión, de otra manera la instrucción anterior produce una excepción, la cual desde luego debería controlarse dentro de un bloque try.
Para enviar datos al servidor a través del socket, vamos a crear un stream de salida de la siguiente manera:
DataOutputStream salida = new DataOutputStream(conexion.getOutputStream());De la misma forma, para leer los datos que envía el servidor a través del socket, creamos un stream de entrada:
DataInputStream entrada = new DataInputStream(conexion.getInputStream());Ahora podemos enviar y recibir datos del servidor. Veamos algunos ejemplos.
Vamos a enviar un entero de 32 bits, en este caso el número 123, utilizando el método writeInt:
salida.writeInt(123);Ahora vamos a enviar un número punto flotante de 64 bits utilizando el método writeDouble:
salida.writeDouble(1234567890.1234567890);Vamos a enviar la cadena de caracteres "hola":
salida.write("hola".getBytes());Debido a que el método write envía un arreglo de bytes, para enviar la cadena de caracteres "hola" es necesario convertirla a arreglo de bytes mediante el método getBytes. Por omisión el método getBytes utiliza la codificación default (UTF-8), para usar otra codificación se puede pasar como parámetro el nombre de la codificación como string, por ejemplo "UTF-8".Ahora supongamos que el servidor envía al cliente una cadena de caracteres. Para que el cliente reciba la cadena de caracteres es necesario que conozca el número de bytes que envía el servidor, en este caso el servidor envía una cadena de caracteres de 4 bytes.
Para recibir los bytes se utiliza el método read de la clase DataInputStream. El método read tiene tres parámetros, el primer parámetro es un arreglo de bytes con una longitud suficiente para contener los bytes a recibir. El segundo parámetro indica la posición, dentro del arreglo de bytes, donde se pondrán los bytes a recibir, y el tercer parámetro indica el número de bytes a recibir.
El siguiente código crea un arreglo de 4 bytes, invoca el método read de la clase DataInputStream, crea una instancia de la clase String utilizando los bytes recibidos.
Debido a que la variable buffer contiene los bytes correspondientes a la cadena de caracteres que envió el servidor, para obtener la cadena de caracteres utilizamos el constructor de la clase String para crear una cadena de caracteres a partir del arreglo de bytes indicando la codificación, en este caso UTF-8.
byte[] buffer = new byte[4]; entrada.read(buffer,0,4); System.out.println(new String(buffer,"UTF-8"));
Sin embargo es necesario considerar que el método read podría obtener solo una parte del mensaje enviado.
Es un error muy común de los programadores creer que el método read siempre regresa el mensaje completo.
En realidad cuando un mensaje es largo, el método read debe ser invocado repetidamente hasta recibir el mensaje completo.
Para recibir el mensaje completo implementaremos un nuevo método read de la siguiente manera:
static void read(DataInputStream f,byte[] b,int posicion,int longitud) throws Exception
{
while (longitud > 0)
{
int n = f.read(b,posicion,longitud);
posicion += n;
longitud -= n;
}
}En este caso, el método estático read regresará el mensaje completo en el arreglo de bytes "b".Para recibir la cadena de caracteres que envía el servidor, vamos a invocar el método estático read:
byte[] buffer = new byte[4]; read(entrada,buffer,0,4); System.out.println(new String(buffer,"UTF-8"));
Los métodos writeUTF y readUTF
Para enviar y recibir strings entre programas escritos en Java, se puede utilizar el método writeUTF de la clase DataOutputStream y el método readUTF de la clase DataInputStream.
El método writeUTF convierte la string a arreglo de bytes utilizando el método getBytes("UTF-8") de la clase String, escribe al stream de salida la longitud del arreglo de bytes utilizando el método writeShort y escribe al stream de salida los bytes utilizando el método write.
El método readUTF lee del stream de entrada el número de bytes a recibir utilizando el método readShort, lee los bytes utilizando el método read y crea una instancia de la clase String utilizando codificación UTF-8.
En Java una string puede tener una longitud máxima de 2,147,483,647 caracteres, sin embargo los métodos writeUTF y readUTF solo pueden enviar strings cuya codificación UTF-8 tenga una longitud máxima de 32,767 bytes.
La clase ByteBuffer
Ahora veremos cómo enviar de manera eficiente un conjunto de números punto flotante de 64 bits.
Supongamos que vamos a enviar cinco números punto flotante de 64 bits.
Primero "empacaremos" los números utilizando un objeto ByteBuffer.
Cinco números punto flotante de 64 bits ocupan 5x8 bytes (64 bits=8 bytes). Entonces vamos a crear un objeto de tipo ByteBuffer con una capacidad de 40 bytes:
ByteBuffer b = ByteBuffer.allocate(5*8);Utilizamos el método putDouble para agregar cinco números al objeto ByteBuffer:
b.putDouble(1.1); b.putDouble(1.2); b.putDouble(1.3); b.putDouble(1.4); b.putDouble(1.5);Para enviar el "paquete" de números, convertimos el objeto BytetBuffer a un arreglo de bytes utilizando el método array de la clase ByteBuffer:
byte[] a = b.array();
salida.write(a);
Thread.sleep(1000); conexion.close();
Servidor.java
El programa Servidor.java va a esperar una conexión del cliente, entonces recibirá los datos que envía el cliente y a su vez, enviará datos al cliente.
Primeramente vamos a crear un socket servidor que va a abrir, en este caso, el puerto 50000:
ServerSocket servidor = new ServerSocket(50000);Notar que en Windows, por razones de seguridad el firewall solicita al usuario administrador permiso para abrir este puerto.
Ahora invocamos el método accept de la clase ServerSocket.
El método accept es bloqueante, lo que significa que el thread principal del programa quedará en estado de espera pasiva (una espera que no ocupa ciclos de CPU) hasta recibir una conexión del cliente. Cuando se recibe la conexión el método accept regresa un socket, en este caso vamos a declarar una variable de tipo Socket llamada "conexion":
Socket conexion = servidor.accept();
DataOutputStream salida = new DataOutputStream(conexion.getOutputStream()); DataInputStream entrada = new DataInputStream(conexion.getInputStream());Recordemos que el cliente envía un entero de 32 bits, entonces el servidor deberá recibir este dato utilizando el método readInt:
int n = entrada.readInt(); System.out.println(n);Ahora el servidor recibe un número punto flotante de 64 bits utilizando el método readDouble:
double x = entrada.readDouble(); System.out.println(x);El servidor recibe una cadena de cuatro caracteres:
byte[] buffer = new byte[4]; read(entrada,buffer,0,4); System.out.println(new String(buffer,"UTF-8"));El servidor envía una cadena de cuatro caracteres:
salida.write("HOLA".getBytes());Ahora vamos a recibir los cinco números punto flotante empacados en un arreglo de bytes.byte[] a = new byte[5*8]; read(entrada,a,0,5*8);
ByteBuffer b = ByteBuffer.wrap(a);Para extraer los números punto flotante, utilizamos el método getDouble de la clase ByteBuffer:
for (int i = 0; i < 5; i++) System.out.println(b.getDouble());
conexion.close();
La clase anterior vimos el programa Servidor.java el cual invoca el método accept para esperar una conexión del cliente, debido a que este método es bloqueante el programa queda en espera pasiva hasta que el cliente se conecta.
Cuando el servidor recibe una conexión, el método accept regresa un socket. Entonces el cliente y el servidor podrán intercambiar datos. Generalmente el servidor procesa los datos que recibe del cliente y al terminar vuelve a invocar el método accept para esperar otra conexión.
Sin embargo, mientras el servidor procesa los datos que recibe del cliente, no puede recibir otra conexión. Para resolver este problema los servidores se construyen utilizando threads.
En la clase de hoy veremos cómo construir un servidor multithread.
Orden de las operaciones de lectura y escritura
En las clases de Sistemas Operativos se explica que un thread (hilo) es la ejecución secuencial de las instrucciones de un programa. Un proceso puede crear uno o más threads (hilos de ejecución), los cuales van a ejecutar simultáneamente.
Si la computadora tiene un CPU dual core, entonces el CPU podrá ejecutar en paralelo (al mismo tiempo) dos threads, si el CPU es quad core entonces podrá ejecutar en paralelo cuatro threads, y así sucesivamente.
Por otra parte, si un programa crea un número de threads mayor al número de procesadores físicos (cores) disponibles en la computadora, entonces los threads ejecutarán en forma concurrente (por turnos).
Los threads dentro de un proceso se comunican entre sí utilizando la memoria. Las operaciones que se realizan sobre la memoria son la lectura y escritura de variables (localidades de memoria).
Para que un thread pueda leer los datos que escribe otro thread en la memoria, es necesario ordenar las operaciones de lectura y escritura que realizan los threads.
Supongamos que tenemos dos threads, el thread T1 y el thread T2. Si el thread T2 escribe a una variable y posteriormente el thread T1 lee la variable, el thread T1 tendrá el valor que escribió el thread T2.
Orden escritura-escritura
Ahora supongamos que los threads T1 y T2 escriben al mismo tiempo una variable y posteriormente los threads leen al mismo tiempo la misma variable. ¿Qué valores leyeron los threads?
Evidentemente no es posible garantizar que el thread T1 haya leído el valor que escribió el thread T2 o que haya leído el valor que escribió el mismo thread T1. Igualmente, no es posible garantizar que el thread T2 haya leído el valor que escribió el thread T1 o que el thread T1 haya el valor que escribió el mismo thread T2.
Entonces, para garantizar que un thread lee el valor de una variable escrito por otro thread, es necesario ordenar las operaciones de escritura y de lectura que realizan los threads.
Ahora supongamos que el thread T1 espera a que el thread T2 escriba la variable, entonces después el thread T1 escribe a la variable.
En este caso, el valor que leen los threads T1 Y T2 es el valor que escribió el thread T1.
Notar que dos o más threads pueden leer simultáneamente una variable.
Orden escritura-lectura
Ahora supongamos que el therad T1 lee la variable y posteriormente el thread T2 escribe la variable ¿qué valor leyó el thread T1?
Para que el thread T1 pueda leer el valor que escribe el thread T2, es necesario ordenar las operaciones de escritura y lectura.
Para garantizar que el thread T1 lea el valor escrito por el thread T2, el thread T1 debe esperar a que el thread T2 escriba la variable.
Cuándo dos o más threads leen o escriben a una misma variable, y al menos uno de los threads escribe la variable, entonces es necesario sincronizar el acceso de los threads a la variable.
Sincronizar el acceso de los threads significa ordenar las operaciones de escritura y de lectura que realizan los threads.
synchronized(objeto)
{
instrucciones
}
La instrucción synchronized funciona de la siguiente manera:
Programación multithread en Java
Supongamos que tenemos una clase llamada P.
Dentro de la clase P definimos una clase interior (nested class) llamada Worker la cual es subclase de la clase Thread:
class P
{
static class Worker extends Thread
{
public void run()
{
}
}
public static void main(String[] args) throws Exception
{
}
}Podemos ver que hemos incluido en la clase Worker un método público llamado run el cual no tiene parámetros ni regresa resultado.Crear un thread e iniciar su ejecución
Para iniciar la ejecución de un thread, debemos crear una instancia de la clase Worker e invocar el método start (este método se hereda de la clase Thread):
Worker w = new Worker(); w.start();Entonces se crea un hilo que inicia invocando el método run que hemos definido en la clase Worker.
Un thread finaliza su ejecución cuando el método run termina. Cuando un thread finaliza, no puede volver a ejecutarse.
El método join
Supongamos que el thread principal (el thread que invocó el método start) requiere esperar que el thread w termine su ejecución, entonces el thread principal deberá invocar el método join:
Worker w = new Worker();El método join queda en un estado de espera pasiva mientras el thread "w" se encuentra ejecutando, cuando el thread "w" termina, el método join regresa, entonces el thread principal continua su ejecución.
w.start();
w.join();
Ahora supongamos que el thread principal requiere crear dos threads y esperar a que terminen su ejecución. Entonces creamos dos instancias de la clase Worker e invocamos los métodos start y join para cada thread:
Worker w1 = new Worker();Cuando un thread (en este caso el thread principal) espera la terminación de uno o más threads para continuar su ejecución, se dice que se implementa una barrera. En este caso estamos implementando una barrera mediante dos métodos join.
Worker w2 = new Worker(); w1.start();
w2.start();
w1.join();
w2.join();
Servidor2.java
Ahora vamos a implementar un servidor de sockets multithread.
class Servidor2
{
static class Worker extends Thread
{
Socket conexion;
Worker(Socket conexion)
{
this.conexion = conexion;
}
public void run()
{
}
}
public static void main(String[] args) throws Exception
{
ServerSocket servidor = new ServerSocket(50000);
for (;;)
{
Socket conexion = servidor.accept();
Worker w = new Worker(conexion);
w.start();
}
}
}Ahora el constructor de la clase Worker pasa como parámetro el socket que crea el método accept, ya que el método run requiere el socket para recibir y enviar datos al cliente.
Ahora agregaremos el siguiente código al método run:
try
{
DataOutputStream salida = new DataOutputStream(conexion.getOutputStream());
DataInputStream entrada = new DataInputStream(conexion.getInputStream());
int n = entrada.readInt();
System.out.println(n);
double x = entrada.readDouble();
System.out.println(x);
byte[] buffer = new byte[4];
read(entrada,buffer,0,4);
System.out.println(new String(buffer,"UTF-8"));
salida.write("HOLA".getBytes());byte[] a = new byte[5*8];
read(entrada,a,0,5*8);
ByteBuffer b = ByteBuffer.wrap(a);
for (int i = 0; i < 5; i++) System.out.println(b.getDouble());
conexion.close();
}
catch(Exception e)
{
System.out.println(e.getMessage());
}
Podemos ver en el programa Servidor2.java que el método run crea los streams que se utilizarán para enviar y recibir datos del cliente. Notar que el programa Servidor2.java es completamente compatible con el programa Cliente.java
Socket conexion = null;
for(;;)
try
{
conexion = new Socket("localhost",50000);
break;
}
catch (Exception e)
{
Thread.sleep(100);
}Como podemos ver, cada vez que el cliente falla en establecer la conexión con el servidor, espera 100 milisegundos y vuelve a intentar la conexión. Cuando el cliente logra conectarse con el servidor entonces sale del ciclo for.La clase de hoy vamos a ver como implementar un cliente y un servidor mediante sockets seguros.
Primeramente vamos a explicar los conceptos básicos de PKI (Public Key Infrastructure).
Criptografía simétrica
La criptografía simétrica es un conjunto de algoritmos que permiten encriptar y desencriptar utilizando la misma clave, conocida como clave secreta o clave privada.
Ejemplos de algoritmos simétricos son el AES-128, AES-256, RC4, RC5, DES, 3DES, entre otros.
Los algoritmos simétricos son muy rápidos, sin embargo resulta complicado intercambiar las claves.
Por ejemplo, supongamos que un amigo se va a estudiar a otro país y en un momento dado te pide le envíes un documento electrónico utilizando email. Desde luego habría que encriptar el documento para evitar que alguna otra persona pudiera hacer mal uso de él.
El problema es que ambos deberían tener la clave para encriptar y desencriptar.
La única solución es que ambos hayan compartido la clave para encriptar y desencriptar antes del viaje, ya que tampoco es seguro enviar la clave por email.
Criptografía asimétrica
En la criptografía asimétrica el destinatario del mensaje tienen dos claves, una llamada clave privada y otra llamada clave pública.
La clave privada es una clave que mantiene en secreto el destinatario de los datos, mientras que la clave pública es una clave que puede conocer cualquiera.
En la criptografía asimétrica (también llamada criptografía de llave pública), el originario utiliza la clave pública del destinatario para encriptar los datos. Entonces el destinatario utiliza su clave privada para desencriptar los datos encriptados y obtener los datos en claro.
El par de claves pública y privada pueden utilizarse para autenticar un documento electrónico. A esta autenticación se llama firma digital.
Supongamos que vamos a enviar un documento (datos) a un destinatario. Para garantizar que el documento procede de un origen determinado, el originario genera un hash de los datos y encripta el hash con su clave privada. Al hash hash encriptado le llamamos la firma digital del documento.
Entonces el originario envía al destinatario el documento y la firma digital respectiva.
El destinatario desencripta la firma digital utilizando la clave pública del originario, entonces obtiene el hash del documento. Así mismo, el destinatario general el hash del documento recibido.
Si los dos hashes son iguales, entonces podemos estar seguros que el documento recibido procede del propietario de la clave pública y que el documento no ha sido modificado, debido a que cualquier modificación en el documento cambiaría el hash.
Ahora bien, ¿cómo sabemos que una clave pública pertenece a una persona u organismo determinado?
Si el originario nos envió su clave pública por email, y sabemos con certeza que la dirección de correo electrónico pertenece a ésta persona, entonces concluimos que la clave pública pertenece a ésta persona de tal manera que podemos verificar la firma digital de cualquier documento que nos envíe.
Sin embargo, no siempre podemos conocer a ciencia cierta que una dirección de correo electrónico pertenece a una persona determinada.
Para resolver el problema de la identidad de una clave pública, se utiliza un documento electrónico llamado certificado digital.
Un certificado digital es un documento electrónico que contiene, entre otros datos: la identidad de una persona u organización, las fechas de validez del certificado, una clave pública de la persona u organización y la firma digital de los datos anteriores.
La clave pública que contiene el certificado está asociada a una clave privada que solo conoce la persona u organización propietaria del certificado digital. Para mayor seguridad, la clave privada generalmente se encripta con una clave simétrica.
El certificado digital es firmado por una autoridad certificadora (CA) la cual es una organización registrada en el sistema operativo de nuestra computadora como una organización de confianza. El sistema operativo cuenta con un repositorio de certificados de confianza; en este repositorio se instalan los certificados de las autoridades certificadoras en las que confiamos.
Es estándar más utilizado para certificados digitales es el X.509
Por default, el sistema operativo incluye en el repositorio de certificados de confianza los certificados de las autoridades certificadoras reconocidas internacionalmente. A estos certificados se le conoce como certificados raíz (root).
Existen dos tipos de certificados, los certificados autofirmados y los certificados firmados por una CA.
Certificado autofirmado
Un certificado autofirmado es aquel que el usuario crea. Al crear un certificado autofirmado se crea un par de claves pública y privada, la clave pública se incluye en el certificado y éste se firma utilizando la clave privada.
Los datos de identidad en el certificado autofirmado los captura el usuario al crear el certificado.
Certificado firmado por una CA
Un certificado firmado por una CA es un certificado que compramos a un proveedor de certificados digitales (p.e. cheapsslsecurity.com).
Existen dos tipos de certificados firmados por una CA, aquellos que verifican dominio y aquellos que adicionalmente verifican la empresa.
Para poder tener un certificado con verificación de dominio, es necesario ser propietario de un dominio.
Por otra parte, para poder tener un certificado con verificación de empresa, es necesario tener una empresa y un dominio.
Cuando se adquiere un certificado firmado por una CA, además del certificado se obtiene un archivo conocido como bundle, el cual contiene los certificados de CA que forman una ruta de certificación desde el certificado emitido, hasta un certificado raíz pre-instalado en la computadora.
Veamos un ejemplo de certificado digital con verificación de dominio, en este caso el dominio es m4gm.com
Cliente - Servidor SSL
Ahora veremos cómo crear un cliente y un servidor los cuales se comuniquen mediante sockets seguros.
Primeramente vamos a crear un certificado autofirmado utilizando el programa keytool incluido en JDK.
keytool -genkeypair -keyalg RSA -alias certificado_servidor -keystore keystore_servidor.jks -storepass 1234567
La opción genkeypair genera un par de claves pública y privada. La clave pública se pone en un certificado autofirmado con un solo elemento en la ruta de certificación. El alias, en este caso "certificado_servidor", define un nombre con el cual vamos a identificar el certificado. Keystore es un archivo (repositorio) donde se va a almacenar el certificado y la clave privada correspondiente. Keyalg es el algoritmo a utilizar para generar el par de claves, en este caso RSA. Storepass es la contraseña para el keystore.
Entonces se deberá capturar lo siguiente:
¿Cuáles son su nombre y su apellido?
[Unknown]: nombre
¿Cuál es el nombre de su unidad de organización?
[Unknown]: unidad
¿Cuál es el nombre de su organización?
[Unknown]: organizacion
¿Cuál es el nombre de su ciudad o localidad?
[Unknown]: CDMX
¿Cuál es el nombre de su estado o provincia?
[Unknown]: CDMX
¿Cuál es el código de país de dos letras de la unidad?
[Unknown]: MX
¿Es correcto CN=nombre, OU=unidad, O=organizacion, L=CDMX, ST=CDMX, C=MX?
[no]: si
Introduzca la contraseña de clave para <certificado_servidor>(INTRO si es la misma contraseña que la del almacén de claves):
IMPORTANTE: Para Java, la clave del certificado <certificado_servidor> y la clave (storepass) del almacén de claves (keystore) deben ser las mismas, en este caso: 1234567
Ahora vamos a obtener el certificado contenido en el keystore.
keytool -exportcert -keystore keystore_servidor.jks -alias certificado_servidor -rfc -file certificado_servidor.pem
La opción exportcert lee del keystore el certificado identificado por el alias y genera un archivo texto que contiene el certificado, en este caso se genera el archivo certificado_servidor.pem
Entonces vamos a crear un keystore que utilizará el cliente, este keystore deberá contener el certificado del servidor:
keytool -import -alias certificado_servidor -file certificado_servidor.pem -keystore keystore_cliente.jks -storepass 123456
La opción import lee el archivo certificado_servidor.pem e inserta el certificado en el keytore keystore_cliente.jks, identificando el certificado mediante el alias. Storepass es la contraseña del keystore.
En la siguiente figura podemos ver que el servidor utilizará el keystore que contiene el certificado del servidor y la clave privada respectiva. El cliente utilizará el keystore que contiene el certificado del servidor.
Por otra parte, también es posible utilizar un certificado firmado por una CA, en este caso será necesario que el keystore que utilizará el servidor contenga los certificados de CA (bundle), el certificado del servidor y la clave privada correspondiente. El keystore que utilizará el cliente deberá contener los certificados de CA (bundle) y el certificado del servidor.
ClienteSSL.java
El cliente debe crear una instancia de la clase SSLSocketFactory:
SSLSocketFactory cliente = (SSLSocketFactory) SSLSocketFactory.getDefault();
Entonces vamos a crear un socket que se conectará al servidor invocando el método createSocket de la clase SSLSocketFactory. En este caso el servidor se llama "localhost" (computadora local) y el puerto abierto en el servidor es el 50000.
Socket conexion = cliente.createSocket("localhost",50000);Abrimos los streams de salida y de entrada como lo hicimos anteriormente.
DataOutputStream salida = new DataOutputStream(conexion.getOutputStream());
DataInputStream entrada = new DataInputStream(conexion.getInputStream());
Ahora podemos enviar datos al servidor, por ejemplo vamos a enviar un double:
salida.writeDouble(123456789.123456789);Para terminar el programa cerramos la conexión con el servidor (al cerrar el socket se cierran también los streams asociados), en este caso vamos a poner un retardo de un segundo antes de cerrar la conexión, para permitir que el servidor tenga tiempo de recibir los datos:
Thread.sleep(1000); conexion.close();
ServidorSSL.java
El servidor debe crear una instancia de la clase SSLServerSocketFactory:
SSLServerSocketFactory socket_factory = (SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
Vamos a crear un socket servidor que va a abrir, en este caso, el puerto 50000 utilizando el método createServerSocket de la clase SSLServerSocketFactory:
ServerSocket socket_servidor = socket_factory.createServerSocket(50000);
Ahora invocamos el método accept de la clase ServerSocket. Cuando se recibe la conexión el método accept regresa un socket:
Socket conexion = socket_servidor.accept();
Abrimos los streams de salida y de entrada:
DataOutputStream salida = new DataOutputStream(conexion.getOutputStream());
DataInputStream entrada = new DataInputStream(conexion.getInputStream());
Ahora podemos recibir datos del cliente, en este caso vamos a recibir un double:
double x = entrada.readDouble();
System.out.println(x);
Finalmente, cerramos la conexión:
conexion.close();
Para ejecutar el servidor se debe indicar el nombre del keystore del servidor y la contraseña:
java -Djavax.net.ssl.keyStore=keystore_servidor.jks -Djavax.net.ssl.keyStorePassword=1234567 ServidorSSL
Para ejecutar el cliente se debe indicar el nombre del keystore del cliente (repositorio de confianza) y la contraseña:
java -Djavax.net.ssl.trustStore=keystore_cliente.jks -Djavax.net.ssl.trustStorePassword=123456 ClienteSSL
En esta actividad vamos a desarrollar un servidor multithread que recibará un archivo del cliente y lo almacenará en el disco local. La comunicación deberá utilizar sockets seguros.
1. Cuando el servidor reciba una conexión del cliente, deberá crear un thread el cual recibirá el nombre del archivo utilizando el método readUTF() de la clase DataInputStream, entonces recibirá la longitud del archivo utilizando el método readInt() de la clase DataInputStream y recibirá el contenido del archivo como arreglo de bytes utilizando el método read() estático que se explicó en clase.
2. El servidor deberá escribir el contenido del arreglo de bytes al disco local, utilizando el siguiente método:
static void escribe_archivo(String archivo,byte[] buffer) throws Exception
{
FileOutputStream f = new FileOutputStream(archivo);
try
{
f.write(buffer);
}
finally
{
f.close();
}
}
3. Se deberá pasar como parámetro al cliente el nombre del archivo a enviar, entonces el cliente deberá leer el archivo del disco local utilizando el siguiente método:
static byte[] lee_archivo(String archivo) throws Exception
{
FileInputStream f = new FileInputStream(archivo);
byte[] buffer;
try
{
buffer = new byte[f.available()];
f.read(buffer);
}
finally
{
f.close();
}
return buffer;
}
4. El cliente deberá enviar al servidor el nombre del archivo utilizando el método writeUTF() de la clase DataOutputStream, deberá enviar la longitud del archivo utilizando el método writeInt() de la clase DataOutputStream y el contenido del archivo utilizando el método write() de la clase DataOutputStream.
static void envia_mensaje(byte[] buffer,String ip,int puerto) throws IOException
{
DatagramSocket socket = new DatagramSocket();
InetAddress grupo = InetAddress.getByName(ip);
DatagramPacket paquete = new DatagramPacket(buffer,buffer.length,grupo,puerto);
socket.send(paquete);
socket.close();
}System.setProperty("java.net.preferIPv4Stack","true"); // sugerencia del alumno Jhonatan Jhair Venegas Perezenvia_mensaje("hola".getBytes(),"230.0.0.0",50000);Primero "empacaremos" los números utilizando un objeto ByteBuffer. Cinco números punto flotante de 64 bits ocupan 5x8 bytes (64 bits=8 bytes). Entonces vamos a crear un objeto de tipo ByteBuffer con una capacidad de 40 bytes:
ByteBuffer b = ByteBuffer.allocate(5*8);
b.putDouble(1.1); b.putDouble(1.2); b.putDouble(1.3); b.putDouble(1.4); b.putDouble(1.5);
envia_mensaje(b.array(),"230.0.0.0",50000);
static byte[] recibe_mensaje(MulticastSocket socket,int longitud_mensaje) throws IOException
{
byte[] buffer = new byte[longitud_mensaje];
DatagramPacket paquete = new DatagramPacket(buffer,buffer.length);
socket.receive(paquete);
return paquete.getData();
}System.setProperty("java.net.preferIPv4Stack","true"); // sugerencia del alumno Jhonatan Jhair Venegas PerezInetAddress grupo = InetAddress.getByName("230.0.0.0");MulticastSocket socket = new MulticastSocket(50000);
socket.joinGroup(grupo);
byte[] a = recibe_mensaje(socket,4); System.out.println(new String(a,"UTF-8"));
byte[] buffer = recibe_mensaje(socket,5*8); ByteBuffer b = ByteBuffer.wrap(buffer); for (int i = 0; i < 5; i++)
System.out.println(b.getDouble());
socket.leaveGroup(grupo); socket.close();
MulticastSocket socket = new MulticastSocket(50000);
InetSocketAddress grupo = new InetSocketAddress(InetAddress.getByName("230.0.0.0"),50000);
NetworkInterface netInter = NetworkInterface.getByName("em1");
socket.joinGroup(grupo,netInter);
byte[] a = recibe_mensaje(socket,4);
System.out.println(new String(a,"UTF-8"));
byte[] buffer = recibe_mensaje(socket,5*8);
ByteBuffer b = ByteBuffer.wrap(buffer);
for (int i = 0; i < 5; i++) System.out.println(b.getDouble());
socket.leaveGroup(grupo,netInter);
socket.close();La memoria cache
Por otra parte, si el CPU requiere escribir una variable, busca la variable en la memoria cache, si existe, escribe el valor de la variable en la cache, si no existe, entonces copia la línea de cache (que contiene la variable) de la memoria RAM a la cache, y luego escribe el valor de la variable que está en la cache.
Debido a que la cache tiene un tamaño limitado (del orden de Megabytes), eventualmente se llenará. Para liberar líneas, la cache escribe a la memoria RAM las líneas menos utilizadas.
Como podemos ver, el CPU nunca lee o escribe datos directamente a la memoria RAM.
Así mismo, la cache nunca lee o escribe variables individuales a la memoria RAM, sino que siempre la transferencia de datos entre la cache y la memoria RAM se realiza en bloques (líneas de cache).
Caso 2. La asistente obtiene cajas de expedientes del archivo.
Entonces la asistente sólo va una vez al archivo. Los expedientes solicitados por el jefe se encuentran en la misma caja. El jefe pide más de una vez el expediente del Sr. González el mismo día.
En el caso 2, la asistente ha descubierto los conceptos de localidad espacial y localidad temporal.
En 2006 aparece en la revista Wired el artículo The Information Factories de George Gilder que describe un nuevo modelo de arquitectura basado en una infraestructura de cómputo ofrecida como servicios virtuales a nivel masivo, a este nuevo modelo se le llamó cloud computing (cómputo en la nube).
El concepto clave en el cómputo en la nube es el "servicio":
Ingresar al portal de Azure en la siguiente URL:
https://azure.microsoft.com/es-mx/features/azure-portal/
1. Dar click al botón "Iniciar sesión".
2. En el portal de Azure seleccionar "Máquinas virtuales".
3. Seleccionar la opción "+Crear".
4. Seleccionar la opción "+Virtual machine"
5. Seleccionar el grupo de recursos o crear uno nuevo. Un grupo de recursos es similar a una carpeta dónde se pueden colocar los diferentes recursos de nube que se crean en Azure.
6. Ingresar el nombre de la máquina virtual.
7. Seleccionar la región dónde se creará la máquina virtual. Notar que el costo de la máquina virtual depende de la región.
8. Seleccionar la imagen, en este caso vamos a seleccionar Ubuntu Server 18.04 LTS.
9. Dar click en "Seleccionar tamaño" de la máquina virtual, en este caso vamos a seleccionar una máquina virtual con 1 GB de memoria RAM. Dar click en el botón "Seleccionar".
10. En tipo de autenticación seleccionamos "Contraseña".
11. Ingresamos el nombre del usuario, por ejemplo: ubuntu
12. Ingresamos la contraseña y confirmamos la contraseña. La contraseña debe tener al menos 12 caracteres, debe al menos una letra minúscula, una letra mayúscula, un dígito y un carácter especial.
13. En las "Reglas de puerto de entrada" se deberá dejar abierto el puerto 22 para utilizar SSH (la terminal de secure shell).
14. Dar click en el botón "Siguiente: Discos>"
15. Seleccionar el tipo de disco de sistema operativo, en este caso vamos a seleccionar HDD estándar.
16. Dar click en el botón "Siguiente: Redes>"
17. Dar click en el botón "Siguiente: Administración>"
18. En el campo "Diagnóstico de arranque" seleccionar "Desactivado".
19. Dar click en el botón "Revisar y crear".
20. Dar click en el botón "Crear".
21. Dar click a la campana de notificaciones (barra superior de la pantalla) para verificar que la maquina virtual se haya creado.
22. Dar click en el botón "Ir al recurso". En la página de puede ver la direción IP pública de la máquina virtual. Esta dirección puede cambiar cada vez que se apague y se encienda la máquina virtual.
23. Para conectarnos a la máquina virtual vamos a utilizar el programa ssh disponible en Windows, Linux y MacOS.
24. En una ventana de comandos de Windows o una terminal de Linux o MacOS ejecutar el programa ssh así:
ssh usuario@ip
Donde usuario es el usuario que ingresamos en el paso 11, ip es la ip pública de la máquina virtual.
25. Para enviar o recibir archivos de la máquina virtual, se puede utilizar el programa sftp disponible en Windows, Linux y MacOS. Se ejecuta así:
sftp usuario@ipPara enviar archivos se utiliza el comando put y para recibir archivos se utiliza el comando get.
Para mayor información sobre sftp ver:
https://www.digitalocean.com/community/tutorials/how-to-use-sftp-to-securely-transfer-files-with-a-remote-server-es
Abrir un puerto de entrada
Para que los programas que ejecutan en la máquina virtual pueda recibir conexiones a través de un determinado puerto, es necesario crear una regla de entrada para el puerto.
Por ejemplo, vamos a abrir el puerto 50000 en la máquina virtual que acabamos de crear:
Detener una máquina virtual
Cuando una máquina virtual no se utiliza es conveniente detenerla con el fin de reducir el costo. Para detener una máquina virtual:
1. Dar click en la opción "Detener" en el portal de Azure.
2. Dar click en el botón "Aceptar".
Esperar a que el estado de la máquina virtual sea "Desasignada".
Encender una máquina virtual
Para encender una máquina virtual
1. Seleccionar la opción "Iniciar" en la página de la máquina virtual dentro del portal de Azure.
Esperar a que el estado de la máquina virtual sea "En ejecución".
Eliminar una máquina virtual
Para eliminar una máquina virtual:
1. Seleccionar la opción "Eliminar" en la página de la máquina virtual dentro del portal de Azure.
2. Dar clieck en el botón "Aceptar".
Los recursos asociados (discos, IP pública, interfaz de red, grupo de seguridad de red, etc.) no se eliminarán, para eliminarlos se deberá seleccionar cada recurso y eliminarlos manualmente.
Para eliminar los recursos asociados a una máquina virtual previamente eliminada:
1. Dar click al icono de "hamburguesa" (las tres líneas horizontales) localizado en la parte superior izquierda de la pantalla.
2. Seleccionar "Todos los recursos".
3. Seleccionar cada recursos (dar click en cada checkbox)
4. Seleccionar "Eliminar".
5. Verificar la lista de recursos a eliminar.
6. Escribir la palabra: sí (con acento en la i).
7. Dar click en el botón "Eliminar".
Ver los videos:
En esta actividad veremos como crear una máquina virtual con Windows, cómo conectarse a la máquina virtual y cómo transferir archivos.
Es muy importante que cada alumno elimine la máquina virtual una vez haya terminado de utilizarla, ya que mantener encendida una máquina virtual genera costo, lo que representa una disminución en el crédito que tiene el alumno como parte del programa Azure for Students.
Creación de una máquina virtual con Windows
1. En el portal de Azure seleccionar "Máquinas virtuales".
2. Seleccionar las opciones "+Crear" y "+Máquina virtual".
3. Seleccionar el grupo de recursos o crear uno nuevo.
4. Ingresar el nombre de la máquina virtual.
5. Seleccionar la región dónde se creará la máquina virtual. Notar que el costo de la máquina virtual depende de la región.
6. Seleccionar la imagen, en este caso vamos a seleccionar Windows Server 2012.
7. Seleccionar el tamaño de la máquina virtual, en este caso vamos a seleccionar una máquina virtual con al menos 2 GB de memoria.
8. Ingresar el nombre del usuario administrador y la contraseña.
9. En las "Reglas de puerto de entrada" se deberá dejar abierto el puerto 3389 para utilizar Remote Desktop Protocol (RDP).
10. Dar click en el botón "Siguiente: Discos>"
11. Seleccionar el tipo de disco de sistema operativo, en este caso vamos a seleccionar HDD estándar.
12. Dar click en el botón "Siguiente: Redes>"
13. Dar click en el botón "Siguiente: Administración>"
14. En el campo "Diagnóstico de arranque" seleccionar "Desactivado".
15. Dar click en el botón "Revisar y crear".
16. Dar click en el botón "Crear".
17. Dar click a la campana de notificaciones para verificar que la maquina virtual se haya creado.
18. Dar click en el botón "Ir al recurso".
19. Seleccionar la opción "Conectar". Seleccionar "RDP".
20. Dar click en el botón "Descargar archivo RDP".
21. Ejecutar "cmd" en la computadora local.
22. Vamos a crear un directorio en la computadora local. La máquina virtual recién creada va a ver este directorio como un disco lógico. Por ejemplo, el directorio se llamará "prueba". Ejecutar el siguiente comando en la ventana de Símbolo del sistema:
mkdir prueba
23. Ahora vamos a crear un disco lógico como alias del directorio creado. Ejecutar el siguiente comando:
subst f: prueba
Podemos ver que el disco lógico aparece en el explorador de archivos de Windows.
24. Buscar el archivo de conexión en la carpeta de descargas (un archivo con el nombre de la máquina virtual y la extensión ".rdp").
25. Dar click derecho al archivo de conexión y seleccionar "Modificar".
26. Seleccionar la pestaña "Recursos locales".
27. Dar click en el botón "Mas..."
28. Abrir la sección "Unidades".
29. Marcar la casilla "Windows (F:)"
30. Dar click en el botón "Aceptar".
32. Ingresar el nombre de usuario administrador y la contraseña.
33. Dar click en el botón "Sí" en la ventana de advertencia. Entonces se abrirá una ventana de escritorio remoto, la cual nos dará acceso al escritorio de la máquina virtual.
34. Configurar los parámetros de privacidad y dar click en el botón "Accept".
35. En la ventana "Networks" dar click en el botón "No".
36. Para ver el disco lógico creado en el paso 23, abrir el explorador de Windows de la máquina virtual. Entonces para enviar archivos desde la computadora local a la máquina virtual se deberá colocar los archivos en el directorio creado en el paso 22, y para enviar archivos desde la máquina virtual a la computadora local se deberá colocar los archivos en el disco F de la máquina virtual.
Nota. El teclado local podría no coincidir con la configuración del teclado de la maquina remota.
37. Para desconectarse de la máquina virtual, dar click en el botón "X" del escritorio remoto. Notar que al cerrar el escritorio remoto la máquina virtual sigue ejecutando.
Vamos a ver cómo crear la imagen de una máquina virtual con Ubuntu en Azure y cómo crear máquinas virtuales a partir de la imagen.
Una máquina virtual generalizada es un máquina virtual cuyo sistema operativo se ha despojado de la configuración específica y la configuración de usuarios.
Una imagen generalizada es la captura de un sistema operativo de una máquina virtual generalizada..
Notas importantes
1. La captura de la imagen de una máquina virtual inutiliza la máquina virtual ya que una máquina virtual generalizada no se puede iniciar o modificar.
2. La generalización de una máquina virtual no implica que se borre toda la información confidencial que pudiera existir en la máquina virtual. Es muy importante considerar lo anterior si se va a re-distribuir la imagen de la máquina virtual.
3. La generalización de una máquina virtual no elimina el archivo /etc/resolv.conf (ver: resolvconf)
4. La generalización de una máquina virtual deshabilita la contraseña de root.
5. La opción +user del comando waagent elimina la última cuenta creada en la máquina virtual incluyendo el directorio del usuario. Si se desea conservar el usuario y el directorio, no se deberá utilizar la opción +user al generalizar la máquina virtual mediante el comando waagent.
6. Para generalizar una máquina virtual con Windows se utiliza el programa sysprep.exe, ver: https://docs.microsoft.com/en-us/azure/virtual-machines/generalize.
7. Una imagen se cobra de acuerdo al espacio en disco que ocupa, ver: Precios de Managed Disks.
Crear la imagen de una máquina virtual con Ubuntu
Para generalizar la máquina virtual utilizaremos el agente waagent el cual elimina los datos específicos de la máquina virtual.
1. Crear una máquina virtual con Ubuntu.
2. Abrir una ventana cmd de Windows o una terminal de Linux o MacOs.
3. Ejecutar el programa ssh en la ventana, pasando como parámetros el usuario (por ejemplo ubuntu) y la ip pública de la máquina virtual:
ssh usuario@ip
4. Para generalizar la máquina virtual y eliminar la última cuenta de usuario creada incluyendo el directorio del usuario, ejecutar el comando:
6. Seleccionar la opción "Captura".
6.1 En la opción "Compartir imagen con Shared Image Gallery" seleccionar "No, capturar solo una imagen administrada".
7. Marcar la casilla "Eliminar automáticamente esta máquina virtual después de crear la imagen", ya que una máquina virtual generalizada no se puede iniciar o modificar.
8. Ingresar el nombre de la imagen a crear.
9. Dar clic en el botón "Crear".
10. Dar clic en la campana de notificaciones para verificar que se haya creado la imagen de la máquina virtual.
Crear una máquina virtual a partir de una imagen
3. Seleccionar el grupo de recursos dónde se creará la máquina virtual.
4. Ingresar el nombre de la máquina virtual.
5. Seleccionar el tamaño de la máquina virtual.
6. Seleccionar el tipo de autenticación (Clave pública SSH o Contraseña). En su caso, ingresar el usuario y contraseña.
7. Dar clic en el botón "Siguiente: Discos >"
8. Seleccionar el tipo de disco del sistema operativo (p.e. HDD estándar).
9. Si no hay otra configuración que se quiera realizar, dar clic en el botón "Revisar y crear".
10. Dar clic en el botón "Crear".
Referencias:
Captura de una imagen administrada de una máquina virtual generalizada en Azure
Información y uso del agente de Linux de Azure
Ahora vamos a ver cómo crear la imagen de una máquina virtual con Windows y cómo crear máquinas virtuales a partir de la imagen.
Notas importantes
1. La captura de la imagen de una máquina virtual inutiliza la máquina virtual ya que una máquina virtual generalizada no se puede iniciar o modificar.
2. La generalización de una máquina virtual no implica que se borre toda la información confidencial que pudiera existir en la máquina virtual. Es muy importante considerar lo anterior si se va a re-distribuir la imagen de la máquina virtual.
3. La generalización de una máquina virtual elimina las variables de ambiente de sistema y de usuario.
4. Una imagen se cobra de acuerdo al espacio en disco que ocupa, ver: Precios de Managed Disks.
Crear la imagen de una máquina virtual con Windows
Para generalizar la máquina virtual utilizaremos el programa sysprep.exe el cual elimina los datos específicos de la máquina virtual.
1. Crear una máquina virtual con Windows Server 2012.
2. Conectarse a la máquina virtual utilizando escritorio remoto.
3. Para generalizar la máquina virtual y así eliminar la información de seguridad y las cuentas de usuarios, dar clic derecho en el botón de inicio de Windows. Entonces seleccionar "Command Promt (Admin)" y ejecutar en la ventana el siguiente programa:
\Windows\System32\Sysprep\sysprep.exe
6. Seleccionar la opción "Captura".
6.1 En la opción "Compartir imagen con Shared Image Gallery" seleccionar "No, capturar solo una imagen administrada".
7. Marcar la casilla "Eliminar automáticamente esta máquina virtual después de crear la imagen", ya que una máquina virtual generalizada no se puede iniciar o modificar.
8. Ingresar el nombre de la imagen a crear.
9. Dar clic en el botón "Revisar y crear".
10. Dar clic en el botón "Crear".
10. Dar clic en la campana de notificaciones para verificar que se haya creado la imagen de la máquina virtual.
11. Eliminar la máquina virtual generalizada.
Crear una máquina virtual a partir de una imagen
3. Seleccionar el grupo de recursos dónde se creará la máquina virtual.
4. Ingresar el nombre de la máquina virtual.
5. Seleccionar el tamaño de la máquina virtual.
6. Seleccionar el tipo de autenticación (Clave pública SSH o Contraseña). En su caso, ingresar el usuario y contraseña.
7. Dar clic en el botón "Siguiente: Discos >"
8. Seleccionar el tipo de disco del sistema operativo (p.e. HDD estándar).
9. Si no hay otra configuración que se quiera realizar, dar clic en el botón "Revisar y crear".
10. Dar clic en el botón "Crear".
En la clase de hoy veremos el tema de sincronización en sistemas distribuidos.
¿Cuándo se requiere sincronizar?
El tiempo es una referencia que utilizan los sistemas distribuidos en varias situaciones.
Supongamos una plataforma de comercio electrónico que funciona a nivel global, en cada país se tiene un servidor con una base de datos dónde se registran las compras, incluyendo la fecha y hora en la que se realiza cada compra.
Para consolidar las compras a nivel mundial cada servidor debe enviar los datos a un servidor central. Sin embargo, no es posible ordenar las compras por fecha debido a dos situaciones:
sudo apt-get update
sudo apt-get install ntp
Como vimos anteriormente, si dos computadoras no interactúan entonces no es necesario que sus relojes estén sincronizados.
La clase de hoy veremos algunos algoritmos que resuelven el problema de exclusión mutua, el cual se presenta cuando dos o más procesadores requieren acceder simultáneamente un recurso compartido (impresora, memoria, CPU, archivo, etc.).
1. Suponga que tiene tres nodos (0, 1 y 2) los cuales implementan el algoritmo distribuido para exclusión mutua de Ricart.
2. Cuando el tiempo lógico del nodo 0 es 8, el tiempo lógico del nodo 1 es 10 y el tiempo lógico del nodo 2 es 12 los tres nodos requieren bloquear un recurso simultáneamente
3. Describir los pasos a seguir de acuerdo al algoritmo distribuido para exclusión mutua.
4. Desarrollar un servidor multithread de acuerdo a las siguientes especificaciones:
El programa deberá hacer lo siguiente:
A partir del servidor multithead desarrollado en la actividad anterior, escribir un programa en Java que muestre el uso del algoritmo del abusón (bully) para la elección de un coordinador en ocho nodos:
El programa deberá hacer lo siguiente:
Ejecutar el programa en ocho ventanas de comandos de Windows, terminales de Linux o MacOS. En cada ventana ejecutar una instancia del programa, en la primera ventana ejecutar el nodo 0, en la segunda ventana ejecutar el nodo 1, en la tercera ventana ejecutar el nodo 2, etc.
Tolerancia a fallas
Las fallas que presenta un canal de comunicación pueden ser fallas por congelación, omisión, tiempo y fallas arbitrarias. Veremos más adelante que las fallas que reciben especial atención son las fallas por congelación y omisión.
Un canal de comunicación confiable es aquel que oculta las fallas en la comunicación.
A partir del servidor multithead desarrollado anteriormente, escribir un programa en Java que implemente el algoritmo de exclusión mutua mediante token en anillo.
El programa funcionará en tres nodos:
El programa deberá hacer lo siguiente:
La clase de hoy vamos a iniciar con el tema Sistemas basados en objetos distribuidos.
Paradigma de paso de mensajesHasta ahora hemos desarrollado programas distribuidos utilizando paso de mensajes.
El paradigma de paso de mensajes es el modelo natural para el desarrollo de sistemas distribuidos, ya que reproduce la comunicación entre las personas.
En el paradigma de paso de mensajes, las computadoras comparten los datos utilizando mensajes. El programador debe serializar los datos antes de enviarlos, y des-serializar los datos después de recibirlos.
El desarrollo de sistemas basados en paso de mensajes es complejo debido a que el programador debe controlar el intercambio de los mensajes, además de desarrollar la funcionalidad propia del sistema.
El paradigma de paso de mensajes es orientado a datos.
Un objeto encapsula variables (campos) y funciones (métodos). Las variables guardan el estado del objeto y los métodos permiten modificar y acceder el estado del objeto.
Los objetos locales comparten el espacio de direcciones, en otras palabras, los objetos locales son objetos que residen en la misma memoria.
Objetos remotos
Un objeto remoto es aquel cuyos métodos son invocados por procesos remotos, es decir, procesos que ejecutan en una computadora remota conectada mediante una red.
Los objetos que se encuentran en diferentes computadoras no comparten el espacio de direcciones, por tanto, solo comparten valores pero no referencias.
La siguiente figura muestra un proceso cliente y un proceso servidor que ejecutan en diferentes computadoras. En el proceso cliente un método local invoca un método remoto, el cual forma parte de un objeto contenido en el proceso servidor.
En este caso, la invocación de los métodos remotos se realiza mediante una capa llamada RMI (Remote Method Invocation).
Paradigma de objetos distribuidos
El paradigma de objetos distribuido combina objetos locales y objetos remotos. La ventaja que tiene, comparado con el paradigma de paso de mensajes, es que el paradigma de objetos distribuidos representa una abstracción sobre el paso de mensajes, por tanto el programador no debe preocuparse por controlar el paso de mensajes entre los nodos.
El paradigma de objetos distribuidos es orientado a la acción, ya que se basa en la acción que realiza el método remoto invocado.
En un sistema que utiliza RMI existe un proceso llamado registry el cual hace las funciones de servidor de nombres.
En cada nodo, hay un proceso servidor el cual registra en el servidor de nombres los objetos que exportará. Cada objeto exportado por el servidor será identificado mediante una URL.
Para acceder a un objeto remoto, el proceso cliente consulta el servidor de nombres utilizando la URL, si el objeto es encontrado, entonces el servidor de nombres regresa al cliente una referencia que apunta al objeto remoto. Entonces el proceso cliente utiliza la referencia para invocar los métodos del objeto remoto, los cuales se ejecutan en el servidor.
El paso de parámetros y regreso de resultado es manejado automáticamente por la capa RMI.
Java RMI
Java RMI es un API que implementa la invocación de métodos remotos. JDK incluye un servidor de nombres llamado rmiregistry, esta aplicación se encuentra en el directorio bin del JDK
¿Cómo usar Java RMI?
Para utilizar Java RMI se debe seguir los siguientes pasos:
1. Para cada objeto remoto se debe crear una interface I que defina el prototipo de cada método a exportar. Es necesario declarar que los métodos remotos pueden producir la excepción java.rmi.RemoteException. La interface I debe heredar de java.rmi.Remote.
2. El código de los métodos remotos se debe escribir en una clase C que implemente la interface I. La clase C debe ser una subclase de java.rmi.server.UnicastRemoteObject. El constructor default de la clase C debe invocar el constructor de la superclase. Es necesario declarar que los métodos remotos pueden producir la excepción java.rmi.RemoteException.
3. El proceso servidor deberá registrar la clase C invocando el método bind() o el método rebind() de la clase java.rmi.Naming. A los métodos bind() y rebind() se les pasa como parámetros la URL correspondiente al objeto remoto y una instancia de la clase C. La URL tiene la siguiente forma: rmi://ip:puerto/nombre, donde ip es la dirección IP de la computadora dónde ejecuta el programa rmiregistry, puerto es el número de puerto utilizado por rmiregistry (se puede omitir si rmiregistry utiliza el puerto default 1099) y nombre es el nombre con el que identificaremos el objeto.
4. El proceso cliente deber invocar el método lookup() de la clase java.rmi.Naming para obtener una referencia al objeto remoto. El método lookup() regresa una instancia de la clase Remote, la cual se debe convertir al tipo de la interface I mediante casting. Utilizando la referencia, el proceso cliente invocará los métodos remotos de la clase C.
Por razones de seguridad, la aplicación rmiregistry se debe ejecutar en la misma computadora dónde ejecuta el servidor.
Por default la aplicación rmiregistry utiliza el puerto 1099, si se utiliza otro puerto, se deberá pasar el número de puerto como argumento al ejecutar rmiregistry.
Se puede notar que el proceso servidor permanece en ejecución debido a que los métodos bind() y rebind() crean threads que no terminan.
Ejemplo de Java RMI
Como vimos anteriormente, para crear una aplicación que utilice Java RMI es necesario crear una interface, una clase, y dos programas (un cliente y un servidor).
En este caso, vamos a crear un objeto remoto que exportará los siguiente métodos:
Primeramente creamos una interface que incluya los prototipos de los métodos a exportar:
public interface InterfaceRMI extends Remote
{
public String mayusculas(String name) throws RemoteException;
public int suma(int a,int b) throws RemoteException;
public long checksum(int[][] m) throws RemoteException;
}
Ahora escribimos la clase ClaseRMI la cual va a contener el código de los métodos definidos en la interface InterfaceRMI. Notar que la clase ClaseRMI es subclase de UnicastRemoteObject e implementa la interface InterfaceRMI.
public class ClaseRMI extends UnicastRemoteObject implements InterfaceRMI
{
// es necesario que el contructor ClaseRMI() invoque el constructor de la superclase
public ClaseRMI() throws RemoteException
{
super( );
}
public String mayusculas(String s) throws RemoteException
{
return s.toUpperCase();
}
public int suma(int a,int b) throws RemoteException
{
return a + b;
}
public long checksum(int[][] m) throws RemoteException
{
long s = 0;
for (int i = 0; i < m.length; i++)
for (int j = 0; j < m[0].length; j++)
s += m[i][j];
return s;
}
}La clase ServidorRMI registra en el rmiregistry una instancia de la clase ClaseRMI utilizando el método rebind().
public class ServidorRMI
{
public static void main(String[] args) throws Exception
{
String url = "rmi://localhost/prueba";
ClaseRMI obj = new ClaseRMI();
// registra la instancia en el rmiregistry
Naming.rebind(url,obj);
}
}El cliente ClienteRMI obtiene una referencia al objeto remoto utilizando el método lookup(), esta referencia es utilizada para invocar los métodos remotos.
public class ClienteRMI { public static void main(String args[]) throws Exception {
// en este caso el objeto remoto se llama "prueba", notar que se utiliza el puerto default 1099 String url = "rmi://localhost/prueba"; // obtiene una referencia que "apunta" al objeto remoto asociado a la URL InterfaceRMI r = (InterfaceRMI)Naming.lookup(url); System.out.println(r.mayusculas("hola")); System.out.println("suma=" + r.suma(10,20)); int[][] m = {{1,2,3,4},{5,6,7,8},{9,10,11,12}}; System.out.println("checksum=" + r.checksum(m)); } }
El día de hoy vamos a explicar cómo ejecutar una aplicación Java RMI en la nube.
Red privada y red pública
Supongamos que creamos dos máquinas virtuales en Azure, cada máquina virtual en un grupo de recursos diferente, esto implica que cada máquina virtual estará conectada a una red virtual (VN) diferente, tal como se muestra en la siguiente figura:
El firewall de una máquina virtual se puede configurar con reglas de entrada y reglas de salida, las reglas de entrada definen qué direcciones públicas y qué puertos se pueden conectar a la máquina virtual, mientras que las reglas de salida definen a qué direcciones públicas y a qué puertos se puede conectar la máquina virtual.
Por seguridad de la máquina virtual, las reglas de entrada suelen ser más restrictivas que las reglas de salida.
En este caso, para que el Nodo-1 se pueda conectar al Nodo-2, solo necesitamos crear una regla de entrada que permita que el Nodo-1 se conecte a través de un puerto específico.
Ahora supongamos que creamos dos máquinas virtuales en el mismo grupo de recursos. En este caso las dos máquinas virtuales comparten la misma red virtual (VN).
Si el Nodo-1 requiere comunicarse con el Nodo-2 no es necesario crear una regla en el firewall del Nodo-2 ya que ambos nodos están conectados a través de la misma red virtual.
Notar que la comunicación entre las máquinas virtuales mediante la VN se realiza utilizando las direcciones IP privadas de las máquinas virtuales.
¿Cómo ejecutar Java RMI en la nube?
{
public static void main(String[] args) throws Exception
{
String url = "rmi://localhost/prueba";
ClaseRMI obj = new ClaseRMI();
// registra la instancia en el rmiregistry
Naming.rebind(url,obj);
}
}
Para que el cliente RMI pueda invocar los métodos del objeto remoto registrado por el servidor RMI, se debe obtiener una referencia al objeto remoto utilizando el método lookup(). Entonces la URL que pasa como parámetro al método lookup() deberá definir la IP privada del nodo dónde ejecuta el servidor RMI.
Supongamos que la dirección IP privada donde ejecuta el servidor RMI, es 10.0.2.4:
{
public static void main(String args[]) throws Exception
{
// en este caso el objeto remoto se llama "prueba", notar que se utiliza el puerto default 1099
String url = "rmi://10.0.2.4/prueba";
// obtiene una referencia que "apunta" al objeto remoto asociado a la URL
InterfaceRMI r = (InterfaceRMI)Naming.lookup(url);
System.out.println(r.mayusculas("hola"));
System.out.println("suma=" + r.suma(10,20));
int[][] m = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
System.out.println("checksum=" + r.checksum(m));
}
}
IMPORTANTE: se debe eliminar las máquinas virtuales y todos sus recursos lo más pronto posible, ya que se deberá ahorrar saldo para poder realizar las tareas siguientes.
En la tarea 3 desarrollamos un programa que multiplica matrices cuadradas en forma distribuida usando paso de mensajes.
Como pudimos ver, la programación de un sistema distribuido utilizando el paso de mensajes es complicada, ya que se debe controlar explícitamente la serialización, el envío y la des-serialización de los datos, además de la lógica particular del sistema.
En la clase de hoy vamos a ver cómo desarrollar un programa distribuido que calcule el producto de matrices cuadradas utilizando Java RMI.
static int[][] multiplica_matrices(int[][] A,int[][] B)
{
int[][] C = new int[N/2][N/2];
for (int i = 0; i < N/2; i++)
for (int j = 0; j < N/2; j++)
for (int k = 0; k < N; k++)
C[i][j] += A[i][k] * B[j][k];
return C;
}static int[][] separa_matriz(int[][] A,int inicio)
{
int[][] M = new int[N/2][N];
for (int i = 0; i < N/2; i++)
for (int j = 0; j < N; j++)
M[i][j] = A[i + inicio][j];
return M;
}static void acomoda_matriz(int[][] C,int[][] A,int renglon,int columna)
{
for (int i = 0; i < N/2; i++)
for (int j = 0; j < N/2; j++)
C[i + renglon][j + columna] = A[i][j];
}El método acomoda_matriz() recibe como parámetros la matriz C, la sub-matriz a acomodar, y la posición (renglón,columna) en la matriz C donde se va a colocar la sub-matriz.
En la clase de hoy vamos a explicar cómo utilizar JSON para serializar y des-serializar objetos.
JSON (JavaScript Object Notation) es un formato texto para el intercambio de datos. JSON corresponde a la sintaxis utilizada en Javascript para escribir objetos.
JSON es un formato independiente del lenguaje de programación, de manera que es posible escribir fácilmente programas en cualquier lenguaje que creen mensajes en formato JSON así como programas que lean mensajes en formato JSON.
En JSON es posible crear dos estructuras: objetos y arreglos.
Un objeto es una colección no ordenada de parejas nombre:valor separadas por coma. Un objeto comienza con una llave que abre “{“ y termina con una llave que cierra “}”.
La sintaxis de un objeto es la siguiente:
Un arreglo es una colección ordenada de valores separados por coma. Un arreglo comienza con un corchete que abre “[“ y termina con un corchete que cierra “]”.
La sintaxis de un arreglo es la siguiente:
Fuente: www.json.org
Un valor puede ser una cadena de caracteres entre comillas, o un número, o un objeto, o un arreglo, o las constantes true, false o null.
La sintaxis de un valor es la siguiente:
Una cadena de caracteres (string) es una secuencia de cero o más caracteres Unicode encerrados entre comillas.
Una cadena de caracteres puede contener las siguientes secuencias de escape:
----------------------------------------------------------------En la clase de hoy veremos los conceptos básicos de los servicios web (Web Services) así como los elementos de servicios web SOAP y REST.
Conceptos básicos de Servicios web
En el documento Web Services Architecture (2004) del World Wide Web Consortium (W2C) define un servicio web como:
“Un sistema de software diseñado para soportar la interacción interoperable de maquina-a-máquina sobre una red. Este cuenta con una interface descrita en un formato el cual puede ser procesado por una computadora (específicamente WSDL). Otros sistemas interactúan con el servicio web en una manera prescrita por su descripción usando mensajes SOAP, típicamente transportados usando HTTP con una serialización XML en conjunción con otros estándares relativos a la Web”.
Un servicio web es un concepto abstracto que debe ser implementado mediante un agente concreto.Un agente es el software o hardware que envía y recibe mensajes. El servicio es el recurso caracterizado por un conjunto abstracto de la funcionalidad que se provee. Un servicio web no cambia aún cuando cambie el agente, es decir, la funcionalidad es independiente de la implementación de ésta.
El propósito de servicio web es proveer cierta funcionalidad a nombre de su propietario (una persona o una organización). La entidad proveedora es aquella persona u organización que provee un agente que implementa un determinado servicio.
Una entidad solicitante es una persona u organización que desea hacer uso del servicio mediante un agente solicitante (también llamado solicitante del servicio) que intercambia mensajes con el agente proveedor (también llamado proveedor del servicio).
En la mayoría de los casos el agente solicitante es el que inicia la comunicación con el agente proveedor, aunque no siempre es así, no obstante se sigue llamando agente solicitante aunque no sea el que inicia la comunicación.
La semántica de un servicio web es la expectativa compartida sobre el comportamiento del servicio, en particular el comportamiento en respuesta a los mensajes que recibe.
Se le llama contrato al acuerdo entre la entidad solicitante y la entidad proveedora. Un contrato puede ser explícito o implícito, escrito u oral, establecido entre las personas y/o las computadoras, legal o informal.
Hay dos tipos de contratos: 1) la descripción del servicio es el contrato que gobierna la mecánica de interacción con un servicio en particular y 2) la semántica del servicio es el contrato que gobierna el significado y propósito de la interacción. Sin embargo, puede haber contratos “híbridos” que incluyan elementos de descripción y elementos de semántica.
Participación en un servicio web
Una entidad solicitante puede participar de un servicio web de diferentes maneras. La siguiente figura muestra el proceso general de participación en un servicio web.
1. Las entidades solicitante y proveedora se conocen una a la otra, o por lo menos una conoce a la otra.Fuente: Web Services Architecture, W3C
SOAP (Simple Object Access Protocol) define un protocolo de RPC (Remote Procedure Call) basado en XML, para la interacción cliente-servidor a través de la red utilizando: 1) HTTP como la base de transporte, y 2) documentos XML para la codificación de requerimientos y respuestas.
SOAP permite la comunicación entre aplicaciones ejecutando en diferentes sistemas operativos, con diferentes tecnologías y lenguajes de programación.
Un mensaje SOAP es un documento XML compuesto por los siguientes elementos:
Para implementar servicios web en Java se puede utilizar la API JAX-WS.
Web Services Description Language (WSDL)
Un documento WSDL es un archivo XML que contiene la descripción de un servicio web SOAP. Este especifica la localización del servicio y los métodos del servicio.
Un cliente puede hacer un requerimiento HTTP a un servicio web SOAP para obtener el WSDL que describe el servicio web.
Los elementos de un documento WSDL son los siguientes:
| Elemento | Descripción |
| <types> | Define los tipos de dato usados por el servicio web |
| <message> | Define los elementos de datos para cada operación |
| <portType> | Describe las operaciones que pueden ser ejecutadas y los mensajes involucrados |
| <binding> | Define el protocolo y el formato de los datos para cada portType |
<definitions xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:wsp="http://www.w3.org/ns/ws-policy" xmlns:wsp1_2="http://schemas.xmlsoap.org/ws/2004/09/policy" xmlns:wsam="http://www.w3.org/2007/05/addressing/metadata" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="http://ws/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.xmlsoap.org/wsdl/" targetNamespace="http://ws/" name="ServicioWebService">
<types>
<xsd:schema>
<xsd:import namespace="http://ws/" schemaLocation="http://localhost:8080/ServicioWeb?xsd=1"/>
</xsd:schema>
</types>
<message name="mayusculas">
<part name="parameters" element="tns:mayusculas"/>
</message>
<message name="mayusculasResponse">
<part name="parameters" element="tns:mayusculasResponse"/>
</message>
<message name="suma">
<part name="parameters" element="tns:suma"/>
</message>
<message name="sumaResponse">
<part name="parameters" element="tns:sumaResponse"/>
</message>
<message name="suma2">
<part name="parameters" element="tns:suma2"/>
</message>
<message name="suma2Response">
<part name="parameters" element="tns:suma2Response"/>
</message>
<portType name="ServicioWeb">
<operation name="mayusculas">
<input wsam:Action="http://ws/ServicioWeb/mayusculasRequest" message="tns:mayusculas"/>
<output wsam:Action="http://ws/ServicioWeb/mayusculasResponse" message="tns:mayusculasResponse"/>
</operation>
<operation name="suma">
<input wsam:Action="http://ws/ServicioWeb/sumaRequest" message="tns:suma"/>
<output wsam:Action="http://ws/ServicioWeb/sumaResponse" message="tns:sumaResponse"/>
</operation>
<operation name="suma2">
<input wsam:Action="http://ws/ServicioWeb/suma2Request" message="tns:suma2"/>
<output wsam:Action="http://ws/ServicioWeb/suma2Response" message="tns:suma2Response"/>
</operation>
</portType>
<binding name="ServicioWebPortBinding" type="tns:ServicioWeb">
<soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/>
<operation name="mayusculas">
<soap:operation soapAction=""/>
<input>
<soap:body use="literal"/>
</input>
<output>
<soap:body use="literal"/>
</output>
</operation>
<operation name="suma">
<soap:operation soapAction=""/>
<input>
<soap:body use="literal"/>
</input>
<output>
<soap:body use="literal"/>
</output>
</operation>
<operation name="suma2">
<soap:operation soapAction=""/>
<input>
<soap:body use="literal"/>
</input>
<output>
<soap:body use="literal"/>
</output>
</operation>
</binding>
<service name="ServicioWebService">
<port name="ServicioWebPort" binding="tns:ServicioWebPortBinding">
<soap:address location="http://localhost:8080/ServicioWeb"/>
</port>
</service>
</definitions>
Servicios web estilo REST
Ver el video:
En esta actividad vamos a crear un servicio web SOAP utilizando JDK8.
1. Primeramente creamos un directorio llamado "ws" en el cual vamos a colocar los programas que vamos a utilizar. Este directorio corresponde al paquete "ws" dónde colocaremos las clases.
2. Vamos a crear el archivo "ServicioWeb.java" en el directorio "ws":
package ws;
import javax.jws.WebService;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import java.util.ArrayList;
import java.util.List;
@WebService
public class ServicioWeb
{
@WebMethod
public double suma(@WebParam(name="a") double a,@WebParam(name="b") double b)
{
return a + b;
}
@WebMethod
public String mayusculas(@WebParam(name="s") String s)
{
return s.toUpperCase();
}
@WebMethod
public List<Integer> suma2(@WebParam(name="a") List<Integer> a,@WebParam(name="b") List<Integer> b)
{
List<Integer> c = new ArrayList<Integer>();
for (int i = 0; i < a.size(); i++)
c.add(a.get(i) + b.get(i));
return c;
}
}
En este caso, nuestro servicio web va incluir las operaciones suma (suma dos números punto flotante), mayúsculas (convierte a mayúsculas una cadena de caracteres) y suyma2 (suma los elementos de dos listas de enteros).
3. Para compilar el servicio web ejecutamos el siguiente comando en el directorio padre del directorio "ws":
javac ws/ServicioWeb.java
4. Para publicar el servicio web utilizamos el programa "Servidor.java" que funciona como servidor de aplicaciones:
package ws;
import javax.xml.ws.Endpoint;
public class Servidor
{
public static void main(String[] args)
{
Endpoint.publish("http://localhost:8080/ServicioWeb",new ServicioWeb());
}
}
5. Para compilar el servidor ejecutamos el siguiente comando en el directorio padre del directorio "ws":
javac ws/Servidor.java
6. Ahora debemos iniciar la ejecución del servidor en otra ventana de cmd en Windows o terminal de Linux o MacOS:
java ws/Servidor
7. Ahora vamos a crea el cliente que consumirá el servicio web. Vamos a utilizar el programa "wsimport" disponible en el JDK8 el cual va a generar las clases que utilizará el cliente. Al programa "wsimport" se le pasa como parámetro la URL del WSDL del servicio web, en este caso la URL es "http://localhost:8080/ServicioWeb?wsdl". Para ver el WSDL de nuestro servicio web, se puede ingresar ésta URL en un navegador.
Nota: antes de ejecutar el siguiente comando hacer una copia del archivo ServicioWeb.java debido a que wsimport lo borra.
Ejecutar el siguiente comando en el directorio padre del directorio "ws":
wsimport "http://localhost:8080/ServicioWeb?wsdl"
8. Vamos a crear el archivo Cliente.java en el directorio "ws":
package ws;
import java.net.URL;
import javax.xml.namespace.QName;
import ws.ServicioWeb;
import ws.ServicioWebService;
import java.util.ArrayList;
import java.util.List;
public class Cliente
{
public static void main(String[] args) throws Exception
{
ServicioWebService s = new ServicioWebService(new URL("http://localhost:8080/ServicioWeb?wsdl"),
new QName("http://ws/","ServicioWebService"));
ServicioWeb obj = s.getServicioWebPort();
System.out.println(obj.suma(100,200));
System.out.println(obj.mayusculas("hola"));
List<Integer> a = new ArrayList<Integer>();
a.add(1);
a.add(2);
a.add(3);
List<Integer> b = new ArrayList<Integer>();
b.add(4);
b.add(5);
b.add(6);
List<Integer> c = obj.suma2(a,b);
for (int i = 0; i < c.size(); i++)
System.out.println(c.get(i));
}
}
9. Para compilar el cliente ejecutamos el siguiente comando en el directorio padre del directorio "ws":
javac ws/Cliente.java
10. Para ejecutar el cliente:
java ws/Cliente
La clase de hoy vamos a implementar un servicio web estilo REST utilizando el API de Java JAX-RS sobre el servidor de aplicaciones Tomcat.
Primeramente instalaremos Tomcat y las bibliotecas necesarias para la implementación de servicios web estilo REST los cuales podrán acceder una base de datos MySQL.
Instalación de Tomcat con soporte REST
1. Crear una máquina virtual con Ubuntu 18 con al menos 1GB de memoria RAM. Abrir el puerto 8080 para el protocolo TCP.
2. Instalar JDK8 ejecutando los siguientes comandos en la máquina virtual:
sudo apt update
sudo apt install openjdk-8-jdk-headless
4. Copiar a la máquina virtual el archivo ZIP descargado anteriormente y desempacarlo utilizando el comando unzip.
5.Eliminar el directorio webapps el cual se encuentra dentro del directorio de Tomcat. Crear un nuevo directorio webapps y dentro de éste se deberá crear el directorio ROOT.
NOTA DE SEGURIDAD: Lo anterior se recomienda debido a que se han detectado vulnerabilidades en algunas aplicaciones que vienen con Tomcat, estas aplicaciones se encuentran originalmente instaladas en los directorios webapps y webapps/ROOT.
6. Descargar la biblioteca "Jersey" de la siguiente URL. Jersey es una implementación de JAX-RS lo cual permite ejecutar servicios web estilo REST sobre Tomcat:
https://repo1.maven.org/maven2/org/glassfish/jersey/bundles/jaxrs-ri/2.24/jaxrs-ri-2.24.zip
7. Copiar a la máquina virtual el archivo descargado anteriormente, desempacarlo y copiar todos los archivos con extensión “.jar” de todos los directorios desempacados, al directorio "lib" de Tomcat.
8. Borrar el archivo javax.servlet-api-3.0.1.jar del directorio "lib" de Tomcat (esto debe hacerse ya que existe una incompatibilidad entre Tomcat y Jersey 2).
9. Descargar el archivo gson-2.3.1.jar de la URL:
https://repo1.maven.org/maven2/com/google/code/gson/gson/2.3.1/gson-2.3.1.jar
10. Copiar el archivo gson-2.3.1.jar al directorio "lib" de Tomcat.
11. Ahora vamos a instalar el driver de JDBC para MySQL. Ingresar a la siguiente URL:
https://dev.mysql.com/downloads/connector/j/
Seleccionar “Platform independent" y descargar el archivo ZIP.
12. Copiar el archivo descargado a la máquina virtual, desempacarlo y copiar el archivo mysql-connector...jar al directorio "lib" de Tomcat.
Iniciar/detener el servidor Tomcat
1. Para iniciar el servidor Tomcat es necesario definir las siguientes variables de entorno:
export CATALINA_HOME=aquí va la ruta del directorio de Tomcat 8
export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
sh $CATALINA_HOME/bin/catalina.sh start
3. Para detener la ejecución de Tomcat se deberá ejecutar el siguiente comando:
sh $CATALINA_HOME/bin/catalina.sh stop
Notar que Tomcat se ejecuta sin permisos de administrador (no se usa "sudo"), lo cual es muy importante para prevenir que algún atacante pueda entrar a nuestro sistema con permisos de super-usuario.
Instalación de MySQL
1. Actualizar los paquetes en la máquina virtual ejecutando el siguiente comando:
sudo apt update
2. Instalar el paquete default de MySQL:
sudo apt install mysql-server
3. Ejecutar el script de seguridad:
sudo mysql_secure_installationPress y|Y for Yes, any other key for No: N contraseña-de-root-en-mysqlRe-enter new password: contraseña-de-root-en-mysql4. Ejecutar el monitor de MySQL:
sudo mysql
5. Ejecutar el siguiente comando SQL para modificar la contraseña de root:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'contraseña-de-root-en-mysql';6. Actualizar los privilegios:FLUSH PRIVILEGES;7. Ejecutar el siguiente comando para salir del monitor de MySQL:quit
Crear un usuario en MySQL
1. Ejecutar el monitor de MySQL:
mysql -u root -p
2. Crea el usuario "hugo":
create user hugo@localhost identified by 'contraseña-del-usuario-hugo';
3. Otorgar todos los permisos al usuario "hugo" sobre la base de datos "servicio_web":
grant all on servicio_web.* to hugo@localhost;
4. Ejecutar el siguiente comando para salir del monitor de MySQL:
quit
Crear la base de datos
1. Ejecutar el monitor de MySQL (notar que ahora se utiliza el usuario "hugo"):
mysql -u hugo -p
2. Crear la base de datos "servicio_web":
create database servicio_web;
3. Conectar a la base de datos creada anteriormente:
use servicio_web;
4. Crear las tablas "usuarios" y "fotos_usuarios", así mismo, se crea una regla de integridad referencial y un índice único:
quit
Compilar, empacar y desplegar el servicio web
1. Descargar de la plataforma y desempacar el archivo Servicio.zip.
2. Definir la variable de ambiente CATALINA_HOME:
export CATALINA_HOME=aquí va la ruta completa del directorio de Tomcat 8
3. Cambiar al directorio dónde se desempacó el archivo Servicio.zip (en ese directorio se encuentra el directorio "negocio").
4. Compilar la clase Servicio.java:
javac -cp $CATALINA_HOME/lib/javax.ws.rs-api-2.0.1.jar:$CATALINA_ HOME/lib/gson-2.3.1.jar:. negocio/Servicio.java
5. Editar el archivo "context.xml" que está en el directorio "META-INF" y definir el username de la base de datos y el password correspondiente. El usuario "hugo" fue creado en el paso 2 de la sección Crear un usuario en MySQL.
6. Ejecutar los siguientes comandos para crear el servicio web para Tomcat (notar que los servicios web para Tomcat son archivos JAR con la extensión .war):
rm WEB-INF/classes/negocio/*
cp negocio/*.class WEB-INF/classes/negocio/.
jar cvf Servicio.war WEB-INF META-INF
7. Para desplegar (deploy) el servicio web, copiar el archivo Servicio.war al directorio "webapps" de Tomcat. Notar que Tomcat desempaca automáticamente los archivos con extensión .war que se encuentran en el directorio webapps de Tomcat.
Para eliminar el servicio web se deberá eliminar el archivo "Servicio.war" y el directorio "Servicio", en éste orden.
Cada vez que se modifique el archivo Servicio.java se deberá compilar, generar el archivo Servicio.war, borrar el archivo Servicio.war y el directorio Servicio del directorio webapps de Tomcat, y copiar el archivo Servicio.war al directorio webapps de Tomcat.
Probar el servicio web utilizando HTML-Javascript
1. Copiar el archivo usuario_sin_foto.png al subdirectorio webapps/ROOT de Tomcat.
Notar que todos los archivos que se encuentran en el directorio webapps/ROOT de Tomcat son accesibles públicamente.
Para probar que Tomcat esté en línea y el puerto 8080 esté abierto, ingresar la siguiente URL en un navegador:
http://ip-de-la-máquina-virtual:8080/usuario_sin_foto.png
2. Copiar el archivo WSClient.js al directorio webapps/ROOT de Tomcat.
3. Copiar el archivo prueba.html al directorio webapps/ROOT de Tomcat.4. Ingresar la siguiente URL en un navegador:
http://ip-de-la-máquina-virtual:8080/prueba.html
5. Dar clic en el botón “Alta usuario” para dar de alta un nuevo usuario. Capturar los campos y dar clic en el botón “Alta”.
6. Intentar dar de alta otro usuario con el mismo email (se deberá mostrar una ventana de error indicando que el email ya existe)
7. Dar clic en el botón “Consulta usuario” para consultar el usuario dado de alta en el paso 5. Capturar el email y dar clic en el botón “Consulta”,
8. Modificar algún dato del usuario y dar clic en el botón “Modifica”:
9. Recargar la página actual y consultar el usuario modificado, para verificar que la modificación se realizó.
10. Dar clic en el botón “Borra usuario” para borrar el usuario. Capturar el email del usuario a borrar y dar clic en el botón “Consulta”.
11. Utilizando un teléfono inteligente y/o una tableta, probar el servicio web accediendo la siguiente URL en un navegador:
http://ip-de-la-máquina-virtual:8080/prueba.html12. Crear una imagen de la máquina virtual conservando el usuario y posteriormente eliminar la máquina virtual y los recursos asociados (excepto la imagen y el grupo de recursos que contiene la imagen). La imagen se utilizará para realizar las tareas 7 y 8.
La clase de hoy explicaremos cómo funciona el servicio estilo REST que implementamos la clase anterior.
En la plataforma se publicó los siguientes archivos:Servicio.zip contiene el código Java y archivos de configuración de un servicio web estilo REST.
prueba.html contiene una aplicación web que invoca el servicio web mediante Javascript.
WSClient.js funciones para invocar el servicio web mediante AJAX
El archivo Servicio.zip contiene los siguientes directorios:
El directorio META-INF contiene el archivo context.xml, en el cual se configura lo siguiente:
El atributo name de la etiqueta Resource, define el nombre del datasource, en este caso el datasource se llama "jdbc/datasource_Servicio".
Un datasource permite configurar las conexiones que realiza el servicio web, sin tener que escribir estos parámetros de configuración en el código (por ejemplo el nombre y contraseña del usuario de la base de datos).
Los atributos que pueden configurarse en un datasource son los siguientes:
El directorio WEB-INF contiene el directorio classes y el archivo web.xml.
El archivo web.xml configura lo siguiente:
La etiqueta load-on-startup igual a 1 indica que el servicio web se debe cargar cuando inicie el servidor de aplicaciones Tomcat.
La etiqueta url-pattern indica la ruta del servicio web. La URL del servicio web se explicará más adelante.
La etiqueta resource-ref, indica el nombre del datasource y la etiqueta res-type indica el tipo de recurso javax.sql.DataSource.
En el directorio classes se colocarán las clases compiladas del servicio web (archivos .class). En este caso el directorio incluye un subdirectorio llamado "negocio", debido a que las clases del servicio web se agrupan en un paquete llamado "negocio".
Al mismo nivel del los directorios META-INF y WEB-INF se encuentra un directorio llamado negocio dónde se puede encontrar los archivos que contienen el código fuente del servicio web.
Los archivos incluidos en el directorio negocio son los siguientes:
AdaptadorGsonBase64.java
La clase AdaptadorGsonBase64 permite modificar la forma en que GSON serializa los campos con tipo byte[].
Por omisión GSON convierte un campo de tipo byte[] en una lista de números separados por comas, lo cual ocupa mucho espacio (y tiempo en la comunicación).
La clase AdaptadorGsonBase64 permite convertir los campos de tipo byte[] a base 64, lo cual produce un texto más compacto.
Notar que jax-rs reemplaza cada "+" por espacio, sin embargo el decodificador Base64 no reconoce el espacio, por lo que es necesario reemplazar los espacios por "+".
Error.java
La clase Error va a permitir regresar un mensaje de error dentro de un objeto.
Foto.java
La clase Foto encapsula un arreglo de bytes y una clave numérica. El servicio web utiliza esta clase para enviar y recibir imágenes.
Usuario.java
La clase Usuario encapsula los datos del usuario. El servicio web utiliza esta clase para recibir los datos del usuario como un objeto.
Es muy importante notar que la anotación @FormParam requiere un método que convierta una String a objeto de tipo Usuario. En este caso se implementa el método valueOf para este propósito.
La clase Servicio implementa los métodos del servicio web.
http://ip-de-la-máquina-virtual:8080/Servicio/rest/ws/consulta_usuario
URL url = new URL("http://ip-de-la-máquina-virtual:8080/Servicio/rest/ws/consulta_usuario");HttpURLConnection conexion = (HttpURLConnection) url.openConnection();
// true si se va a enviar un "body", en este caso el "body" son los parámetros
conexion.setDoOutput(true);
// en este caso utilizamos el método POST de HTTP
conexion.setRequestMethod("POST");
// indica que la petición estará codificada como URL
conexion.setRequestProperty("Content-Type","application/x-www-form-urlencoded");
// el método web "consulta_usuario" recibe como parámetro el id de un usuario, en este caso el id es 10
String parametros = "id=" + URLEncoder.encode("10","UTF-8");
OutputStream os = conexion.getOutputStream();
os.write(parametros.getBytes());
os.flush();
// se debe verificar si hubo error
if (conexion.getResponseCode() == 200)
{
// no hubo errorBufferedReader br = new BufferedReader(new InputStreamReader((conexion.getInputStream())));String respuesta;// el método web regresa una string en formato JSON}
while ((respuesta = br.readLine()) != null) System.out.println(respuesta);
else
{
// hubo error
BufferedReader br = new BufferedReader(new InputStreamReader((conexion.getErrorStream())));
String respuesta;
// el método web regresa una instancia de la clase Error en formato JSON
while ((respuesta = br.readLine()) != null) System.out.println(respuesta);
}
conexion.disconnect();
Servicio web con transacciones.
1. Crear el archivo /etc/rc.local ejecutando el siguiente comando:
sudo vi /etc/rc.local
2. Agregar al archivo lo siguiente:
#!/bin/bash runuser -l ubuntu -c 'export JAVA_HOME=/usr;export CATALINA_HOME=/home/ubuntu/apache-tomcat-8.5.63;sh $CATALINA_HOME/bin/catalina.sh start' exit 0
3. Guardar el archivo.
NOTA. El archivo /etc/rc.local se ejecuta con permisos de root, sin embargo no es conveniente iniciar Tomcat con permisos de super usuario. Para ejecutar Tomcat con permisos de usuario se utiliza el comando runuser, en este caso el usuario que ejecuta Tomcat es "ubuntu".
NOTA. En este caso /home/ubuntu/apache-tomcat-8.5.63 es la ruta absoluta del directorio donde se encuentra Tomcat y /usr es la ruta absoluta donde se encuentra el JDK.
4. Ejecutar el siguiente comando para hacer ejecutable el archivo /etc/rc.local:
sudo chmod +x /etc/rc.local
Ahora cada vez que encienda la máquina virtual iniciará automáticamente Tomcat.
NOTA. Si se crea una imagen de la máquina virtual el archivo /etc/rc.local se guardará en la imagen de manera que al crear máquinas virtuales a partir de la imagen, en todas ellas iniciará automáticamente Tomcat.
Para poder establecer una conexión segura (https) entre un cliente (navegador) y un servidor (p.e. Tomcat), es necesario instalar en el servidor un certificado digital emitido por una autoridad certificadora reconocida.
Al iniciar la comunicación con el servidor, el cliente (navegador) recibe una copia del certificado digital del servidor, éste documento electrónico contiene la llave pública que permitirá establecer la comunicación encriptada con el servidor, el cual cuenta con una llave privada y la correspondiente contraseña.
Para poder obtener un certificado digital es necesario contar con una computadora conectada a Internet, con una IP estática (una dirección IP fija). También es necesario contar con un dominio (un nombre de la forma: mi_empresa.com, mi_empresa.com.mx, etc) asociado a la dirección IP del servidor. El dominio tiene un costo anual, y puede ser contratado con una empresa registradora de dominios autorizada. Así mismo, el certificado digital también tiene un costo anual, y puede adquirirse con un proveedor de certificados.
Hay dos tipos de certificados digitales: 1) certificados con validación de dominio y 2) certificados con validación de empresa. El primer certificado sólo requiere tener un dominio registrado, mientras que el segundo certificado requiere tener una empresa establecida.
En éste procedimiento se mostrará cómo instalar un certificado con validación de dominio en Tomcat.
Adquisición del certificado digital
1. Buscar un proveedor de certificados, por ejemplo www.cheapssls.com
2. Para realizar la conexión segura, Tomcat utilizará un almacén de claves (keystore). Para generar el almacén de claves hay dos posibilidades: 1) descargar la llave privada directamente del proveedor de certificados, posteriormente descargar el certificado emitido y crear el keystore importando la llave privada y el certificado emitido, o bien, 2) generar un requerimiento de certificación, enviar el requerimiento de certificación al proveedor de certificados, posteriormente descargar el certificado emitido, e importar el certificado emitido al keystore que se creó al generar el requerimiento de certificación.
Opción 1. Descargar la llave privada del proveedor de certificados
El proveedor de certificados crea la llave privada y el certificado, entonces el cliente puede descargar la llave privada. Después de validar el dominio (ver mas adelante la sección Validación del dominio), el proveedor de certificados enviará el certificado al cliente junto con un “bundle” que contiene los certificados que componen la ruta de certificación.
Para generar el keystore que utilizará Tomcat es necesario crear un keystore PKCS12, importando la llave privada y el certificado:
openssl pkcs12 -export -in mi_dominio_com.crt -inkey mi_dominio_com_key.txt -chain -CAfile mi_dominio_com.ca-bundle -out mi_dominio.p12 -name mi_llave
Aquí “mi_dominio_com.crt” es el certificado emitido, “ mi_dominio_com_key.txt” es la llave privada descargada de la página del proveedor de certificados, “ mi_dominio_com.ca-bundle” es el bundle que envió el proveedor de certificados junto con el certificado emitido, “mi_dominio.p12” es el almacén de llaves PKCS12 a crear. “mi_llave” es el nombre del almacén de llaves.
Finalmente, para crear el keystore, se ejecuta el siguiente comando:
keytool -importkeystore -destkeystore mi_dominio.jks -srckeystore mi_dominio.p12 -srcstoretype PKCS12 -alias mi_llave
Donde “mi_dominio.jks “ es el keystore a crear y “mi_llave” es el alias que utilizará Tomcat para acceder el certificado y la llave privada que se encuentran en el keystore.
Opción 2. Generar requerimiento de certificación
Para generar un requerimiento de certificación, primero debemos crear un keystore:
keytool -genkeypair -alias mi_llave -keyalg RSA -keystore mi_dominio.jks -keysize 2048 -sigalg SHA256withRSAEn esta caso “mi_llave” el alias que utilizará Tomcat para acceder el certificado y la llave privada que se encuentran en el keystore, y “mi_dominio.jks” es el nombre del keystore a crear. El algoritmo de firma SHA256withRSA permitirá utilizar cripotografía AES a 256 bits. (Nota: no se soporta SHA512withRSA).
Ingresar la contraseña para el almacén de claves.
Responder las siguientes preguntas:
¿Cuáles son su nombre y su apellido? Ingresar el nombre del dominio ¿Cuál es el nombre de su unidad de organización? Dejar vacío ¿Cuál es el nombre de su organización? Ingresar el nombre del negocio o dejar vacío ¿Cuál es el nombre de su ciudad o localidad? Ingresar: CDMX ¿Cuál es el nombre de su estado o provincia? Ingresar: CDMX ¿Cuál es el código de país de dos letras de la unidad? Ingresar: MX
Introduzca la contraseña de clave para <certificado_servidor>
(INTRO si es la misma contraseña que la del almacén de claves):
Recordemos que para Java la contraseña del certificado debe ser la misma que la contraseña del almacén de claves.
Generar el requerimiento de certificado (en este caso el archivo se llama “mi_dominio.csr”), el cual será utilizado por el proveedor de certificados digitales para crear el certificado digital:
keytool -certreq -keyalg RSA -alias mi_llave -file mi_dominio.csr -keystore mi_dominio.jksEl archivo “mi_dominio.csr” contiene un documento en base 64 el cual es de la forma:
-----BEGIN NEW CERTIFICATE REQUEST----- ... -----END NEW CERTIFICATE REQUEST-----
Después de validar el dominio (ver mas adelante la sección Validación del dominio), el proveedor de certificados enviará el certificado al cliente junto con un “bundle” que contiene los certificados que componen la ruta de certificación.
Para importar los certificados que están en el bundle:
keytool -import -alias bundle -trustcacerts -file mi_dominio_com.ca-bundle -keystore mi_dominio.jks
Aquí “mi_dominio.jks “ es el keystore creado anteriormente y “mi_dominio_com.ca-bundle” es el bundle que envió el proveedor de certificados junto con el certificado emitido
Finalmente, para importar el certificado emitido:
keytool -import -alias mi_llave -trustcacerts -file mi_dominio.crt -keystore mi_dominio.jks Donde “mi_dominio.jks “ es el keystore creado anteriormente y “mi_llave” es el alias que utilizará Tomcat para acceder el certificado y la llave privada que se encuentran en el keystore "mi_dominio.jks".
Validación del dominio
Para validar el dominio, el proveedor de certificador puede enviar un correo electrónico o se puede descargar un archivo (validation file) el cual se podrá instalar en el servidor Tomcat que ejecuta en el dominio.
1. Validación del dominio por email
Para validar el dominio el proveedor requiere la cuenta de correo electrónico asociada al registro del dominio o bien, una cuenta de correo que incluya el dominio (por ejemplo webmaster@mi_dominio.com si el dominio es mi_dominio). Entonces el proveedor de certificados enviará un correo de validación y posteriormente otro correo incluyendo el certificado correspondiente al dominio y los certificados intermedios (notar que el certificado es un documento público por lo tanto, no es un riesgo enviarlo vía correo electrónico).
2. Validación del dominio por archivo
Descargar el archivo de validación de la página del proveedor de certificados. Colocar el archivo de validación en el directorio webapps/ROOT de Tomcat, de acuerdo a la URL que indique el proveedor de certificados. Notar que la URL se accede mediante http no https ya que no tenemos todavía instalado un certificado en Tomcat.
Es necesario abrir el puerto 80 y mapear el puerto 80 al 8080 ejecutando el siguiente comando:
sudo iptables -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080 -t nat
Ahora se deberá probar la URL del archivo de validación en un navegador, de manera que se pueda verificar que el proveedor podrá acceder a éste archivo.
Instalación del certificado digital en Tomcat
Para instalar el certificado digital en Tomcat, se debe agregar las siguientes líneas al archivo server.xml que se encuentra en el subdirectorio conf del directorio de Tomcat:
<Connector port="8443"
protocol="HTTP/1.1" SSLEnabled="true"
maxThreads="150"
scheme="https" secure="true"
clientAuth="false"
sslProtocol="TLSv1.2"
keystoreFile="ruta absoluta del archivo mi_dominio.jks"
keystorePass="contraseña del keystore"
keyAlias="mi_llave"
ciphers="ECDHE_RSA_WITH_AES_256_CBC_SHA384,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
TLS_RSA_WITH_AES_256_CBC_SHA256,TLS_RSA_WITH_AES_256_CBC_SHA"/>NOTAS IMPORTANTES:
1. El puerto por default para HTTPS es el 443. Se recomienda por razones de seguridad, que el servidor Tomcat sea ejecutado por un usuario no administrador, sin embargo en Linux el puerto 443 solo lo puede abrir el usuario "root", por tanto Tomcat se deberá iniciar con un usuario no administrador, entonces se deberá utilizar el puerto 8443 en lugar del puerto 443. Para esto, se deberá ejecutar el siguiente comando de manera que el cliente se conecte a Tomcat utilizando el puerto estándar 443:
sudo iptables -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8443 -t nat
El comando anterior se deberá agregar al archivo /etc/rc.local para que se ejecute cada vez que se enciende la máquina virtual.
2. La definición de los cifradores ("ciphers") es opcional. El ejemplo muestra cómo forzar al cliente a utilizar solamente AES256, denegando AES128, sin embargo ... Java 7 y 8 no soportan AES256 en su distribución estándar (para Android se deberá verificar el soporte para AES256 debido a que algunas versiones solo soportan Java 7), ya que se produce el error handshake_failure. Para que un cliente Java soporte AES256 habría que descargar los archivo local_policy.jar y US_export_policy.jar y copiarlos al directorio lib/security del JRE que utilice el cliente. Ver el artículo: Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files 7 Download
http://stackoverflow.com/questions/12192616/jsse-configuration-in-tomcat-enable-aes256-cipher
3. Por razones de seguridad se recomienda deshabilitar el puerto 8080, por tanto habría que comentar las siguientes líneas en el archivo server.xml:
<Connector port="8080"
protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"/>4. Generalmente los proveedores de certificados ofrecen la posibilidad de la re-emisión sin costo de los certificados, esto permite que se solicite un nuevo certificado cuándo sea necesario, por ejemplo cuándo se ha comprometido la llave privada o la contraseña. El procedimiento de re-emisión es el mismo que el procedimiento de emisión.
Nombre, identificador y dirección
Un nombre, en el contexto de un sistema distribuido, es una cadena de caracteres que hace referencia a una entidad o recurso (servidor, impresora, archivo, disco, página web, etc).
Se entiende como punto de acceso a una entidad como el dispositivo desde el cual se tiene acceso a la entidad. Por ejemplo, una computadora es el punto de acceso a los archivos que contiene. Al nombre de un punto de acceso se le llama dirección.
Por ejemplo, la dirección de un servicio que ofrece un servidor es el end point del servicio (dirección IP y puerto).
Un identificador es un nombre que tiene las siguientes propiedades:
En general una dirección no es un identificador, ya que la dirección de una entidad puede cambiar (por ejemplo las direcciones IP dinámicas son modificadas por los proveedores de servicios de Internet).
Por otra parte, el nombre de dominio es en efecto un identificador.
Espacios de nombres
Los nombres se organizan en una estructura llamada espacio de nombres.
Un espacio de nombres es un grafo etiquetado dirigido con dos tipos de nodos: nodos hoja y nodos directorio.
Un grafo etiquetado dirigido está compuesto por nodos conectados por arcos (o aristas). Los arcos tienen una dirección (cada arco tiene una flecha) y una etiqueta. Un arco es de salida si apunta afuera del nodo y es de entrada si apunta adentro del nodo.
Por ejemplo, el siguiente grafo representa un sistema de archivos:
Un nodo directorio contiene una tabla llamada tabla de directorio la cual contiene pares (etiqueta, identificador de nodo), cada etiqueta corresponde a un arco de salida del nodo directorio y cada identificador corresponde al nombre del nodo al que conecta.
Un nodo directorio puede conectar a otro nodo directorio o a un nodo hoja. Al nodo que sólo tiene arcos de salida se le llama nodo raíz. Un grafo que representa un espacio de nombres puede tener más de un nodo raíz, aunque por simplicidad los espacios de nombres tienen un sólo nodo raíz.
En un grafo de nombres cada nodo hoja corresponde a una entidad, en el ejemplo anterior cada entidad es un archivo.
El nombre completo de un nodo hoja en un espacio de nombres se compone de la secuencia de etiquetas de los arcos, iniciando con el nombre del nodo raíz:
N: etiqueta1, etiqueta2, …, etiquetan
Donde N es el primer nodo de la ruta. Por ejemplo, el archivo “xyz” tiene la siguiente ruta desde el nodo n0:
n0: tmp, xyz
A esta secuencia se le llama nombre de ruta.
Si el primer nodo de la ruta es el nodo raíz se le llama nombre de ruta absoluto, de otra manera, se le llama nombre de ruta relativo.
En un sistema de archivos, al primer nodo se le llama generalmente disco lógico y la secuencia de etiquetas se separa por una diagonal “/”, entonces el nombre de ruta es una cadena de caracteres que corresponde a la secuencia de etiquetas.
En general, los recursos tales como procesos, dispositivos de E/S, memoria, etc. forman parte de un espacio de nombres, por tanto, también se puede aplicar cadenas de caracteres como nombres de ruta.
Árbol de nombres
Un árbol de nombres es un grafo de nombres dónde cada nodo tiene exactamente un arco de entrada (un nodo padre), excepto el nodo raíz el cual no tiene arco de entrada.
El ejemplo mostrado anteriormente es un árbol de nombres.
Grafo dirigido no cíclico
En un grafo dirigido no cíclico un nodo puede tener más un arco de entrada, pero no se permite que haya ciclos.
En este grafo podemos ver que el nodo 11 puede ser accedido utilizando dos rutas. Este es el caso de los archivos con vínculos (links) en los sistemas de archivos modernos. Podemos ver que no se trata de un árbol de nombres debido a que en este caso hay nodos que pueden tener más de un arco de entrada (más de un nodo padre).
Ahora vamos a ver cómo se resuelven los nombres en un espacio de nombres y cómo se implementa un espacio de nombres distribuido.
Resolución de nombres
Una de las aplicaciones de los espacios de nombres es el almacenamiento y recuperación de recursos mediante nombres.
En general, dado el nombre de ruta deberá ser posible encontrar el recurso asociado al nombre. Al proceso de búsqueda de un nombre en un espacio de nombres se le llama resolución de nombre.
Supongamos que tenemos un nombre de ruta de la forma:
N: etiqueta1, etiqueta2, etiqueta2, … ,etiquetan
La resolución de nombre inicia en el nodo N, entonces se busca la etiqueta1 en la tabla de directorio del nodo N, si existe, se obtiene el identificador del nodo siguiente. Ahora se busca la etiqueta2 en la tabla de directorio del nodo actual, si existe, se obtiene el identificador del nodo siguiente.
El proceso continúa hasta encontrar el nodo correspondiente a la etiquetan
Mecanismo de clausura
Se le llama mecanismo de clausura a la selección del nodo inicial dentro de un espacio de nombres en el cual comienza la resolución de nombre.
Un mecanismo de clausura es implícito al proceso de resolución de nombre, lo cual significa que el mecanismo de clausura debe estar definido e implementado antes de iniciar el proceso de resolución.
Por ejemplo, si el primer nodo de un nombre de ruta es el nodo raíz, entonces sabemos que el nodo raíz será un nodo directorio dónde se realizará la búsqueda de la primera etiqueta.
Si el primer nodo del nombre de ruta no es el nodo raíz, entonces deberá existir una forma de saber cómo encontrar ese nodo inicial. En un sistema de archivos de tipo Unix, si se quiere resolver utilizando un nombre de ruta relativo, el mecanismo de clausura queda definido por el directorio actual (working directory) el cual se obtiene mediante el comando pwd.
Vinculo absoluto y vinculo simbólico
Un alias es un segundo nombre para una misma entidad en un espacio de nombres.
Existen dos formas de implementar un alias para una entidad: vinculo absoluto (hard link) y vinculo simbólico (symbolic link).
Un vinculo absoluto es simplemente un segundo nombre de ruta para una entidad en el espacio de nombres.
Un vinculo simbólico consiste en almacenar en un nodo hoja el nombre de ruta absoluto correspondiente a la entidad. Para encontrar la entidad se recorre el nombre de ruta hasta el nodo hoja que contiene el nombre de ruta absoluto, entonces se busca la entidad utilizando este nombre absoluto.
En el siguiente ejemplo podemos ver que el archivo “temp” puede ser resuelto mediante la ruta n0:tmp,temp y mediante la ruta n0:temp. En este caso se trata de un vinculo absoluto.
Por otra parte, el archivo “hosts” puede ser resuelto mediante la ruta n0:hosts y mediante la ruta n0:etc,hosts. En este caso se trata de un vínculo simbólico, ya que la resolución de la ruta n0:hosts lleva al nodo hoja n12 dónde se obtiene la ruta absoluta del nodo n7.
Montar un espacios de nombres
La resolución de nombres que vimos anteriormente puede ser utilizada para enlazar diferentes espacios de nombres en forma transparente.
Montar un espacio de nombres B en un espacio de nombres A consiste en hacer que un nodo directorio del espacio de nombres A incluya el identificador de un nodo directorio del espacio de nombres B.
Al nodo directorio del espacio de direcciones A que contiene el identificador del nodo externo se le llama punto de montaje. Así mismo, se le llama punto de montaje al nodo directorio del espacio de direcciones B.
En general, el punto de montaje externo es un nodo raíz.
El concepto de montaje de espacios de nombre permite implementar sistemas de espacios de nombres distribuidos, dónde cada computadora podría administrar un espacio de nombres local.
Para montar un espacio de nombres externo se requiere al menos de lo siguiente:
El nombre del protocolo de comunicación define cómo se va a comunicar la computadora local con la computadora remota.
El nombre de la computadora puede ser la dirección IP de la computadora o bien el nombre de dominio el cual puede ser resuelto mediante un servidor de nombres de dominio (DNS).
Finalmente, deberá existir un mecanismo de clausura en la computadora remota que resuelva el punto de montaje.
Recordemos que ya utilizamos un servidor de nombres llamado rmiregistry, donde se identifican los objetos remotos mediante una URL de la forma: rmi://ip-del-servidor/nombre-del-objeto.
La clase de hoy veremos un ejemplo de espacio de nombres: el sistema de archivos distribuido NFS.
El sistema de archivos distribuidos NFS
El sistema de archivos NFS (Network File System) permite que una computadora (cliente) tenga acceso de manera transparente a los archivos contenidos en un servidor remoto.
Supongamos que una computadora va a acceder mediante NFS los archivos que se encuentran en el directorio /home/usuario de un servidor remoto.
Si el dominio del servidor remoto es m4gm.com, para que el cliente tenga acceso al directorio remoto es necesario montar el espacio de nombres remoto en el espacio de nombres local.
Para montar el espacio de nombres remoto se elige un punto de montaje en el cliente, por ejemplo se puede elegir el directorio /usr. Entonces el nodo directorio correspondiente a /usr va a contener una URL que define el protocolo, el dominio del servidor y el punto de montaje en el servidor, en este caso la URL sería:
nfs://m4gm.com/home/usuario
En general un cliente indica qué archivo va a acceder utilizando una URL de la forma:
nfs://dominio-o-ip-del-servidor/punto-de-montaje
En la figura se puede ver que el nodo directorio n4 contiene la URL que define el punto de montaje del espacio de nombres remoto.
Si el cliente requiere acceder al archivo remoto /home/usuario/listado.txt solo tiene que acceder al “archivo local” /usr/listado.txt
La localización del archivo listado.txt se realiza en tres pasos:
La ventaja de utilizar archivos remotos mediante NFS es que el usuario accede a los archivos sin preocuparse por los detalles de la comunicación entre el cliente y el servidor.
En esta actividad vamos a instalar NFS en dos máquinas virtuales en la nube.
Primeramente necesitamos crear dos máquinas virtuales (cliente y servidor) en Azure con Ubuntu 18 con las siguientes características:
Instalación en el servidor
1. Para instalar NFS en el servidor se ejecutan los siguientes comandos:
sudo apt update
sudo apt install nfs-kernel-server
2. Crear el directorio compartido en el servidor;
sudo mkdir /var/nfs -p
3. El propietario del directorio /var/nfs es root debido a que este directorio se creó con sudo. Podemos ver el propietario del directorio /var/nfs ejecutando el comando:
ls -l /var
4. Debido a que NFS convierte el acceso del usuario root en el cliente en un acceso con el usuario "nobody:nogroup" en el servidor, es necesario cambiar el propietario y permisos del directorio creado anteriormente:
sudo chown nobody:nogroup /var/nfs
sudo chmod 777 /var/nfs
5. Podemos verificar el nuevo propietario:
ls -l /var
6. Ahora se debe registrar el directorio creado en el archivo de configuración de NFS.
6.1 Editar el archivo /etc/exports:
sudo vi /etc/exports
6.2 Agregar la siguiente línea, guardar y salir del editor (en este caso 40.76.45.28 es la IP del cliente):
/var/nfs 40.76.45.28(rw,sync,no_subtree_check)
Para una descripción de los permisos se puede consultar Understanding the /etc/exports File y exports linux page.
6.3 Actualizar la tabla de file systems exportados por NFS:
sudo exportfs -ra
6.4 Para ver los file systems exportados por NFS:
sudo exportfs
6.5 Para activar la nueva configuración, se requiere reiniciar el servidor NFS:
sudo systemctl restart nfs-kernel-server
7. Ahora debemos abrir el puerto 2049 en el portal de Azure:
Intervalos de puertos de destino: 2049
Protocolo: TCP
Nombre: puerto_nfs
Instalación en el cliente
8. Para instalar NFS en el cliente se ejecutan los siguientes comandos:
sudo apt update
sudo apt install nfs-common
9. Crear el directorio de montaje en el cliente (punto de montaje):
sudo mkdir -p /nfs
10. Montar el directorio remoto (en este caso 40.87.94.140 es la IP del servidor):
sudo mount -v -t nfs 40.87.94.140:/var/nfs /nfs
11. Para desmontar el directorio remoto /nfs:
sudo umount /nfsProbar el acceso a archivos remotos
11. En el servidor crear un archivo, en este caso utilizamos el editor vi:
vi /var/nfs/texto.txt
12. Escribir un texto (como el siguiente) guardar y salir de la edición:
Esta es una prueba
13. En el cliente editar el archivo /nfs/texto.txt:
vi /nfs/texto.txt
14. Agregar la siguiente línea, guardar y salir de la edición:
Esta es otra prueba
15. En el servidor desplegar el contenido del archivo /var/nfs/texto.txt:
more /var/nfs/texto.txt
16. En el cliente desplegar el contenido del directorio /nfs:
ls -l /nfs
Para encriptar el tráfico entre el cliente y el servidor NFS se puede utilizar un túnel SSH. Ver: Mount NFS Folder via SSH Tunnel.
Instalación en el servidor
1. Para instalar NFS en el servidor se ejecutan los siguientes comandos:
sudo apt update
sudo apt install nfs-kernel-server
sudo apt install portmap
2. Crear el directorio compartido en el servidor;
sudo mkdir /var/nfs -p
3. El propietario del directorio /var/nfs es root debido a que este directorio se creó con sudo. Podemos ver el propietario del directorio /var/nfs ejecutando el comando:
ls -l /var
4. Debido a que NFS convierte el acceso del usuario root en el cliente en un acceso con el usuario "nobody:nogroup" en el servidor, es necesario cambiar el propietario y permisos del directorio creado anteriormente:
sudo chown nobody:nogroup /var/nfs
sudo chmod 777 /var/nfs
5. Podemos verificar el nuevo propietario:
ls -l /var
6. Ahora se debe registrar el directorio creado en el archivo de configuración de NFS.
6.1 Editar el archivo /etc/exports:
sudo vi /etc/exports
6.2 Agregar la siguiente línea, guardar y salir del editor:
/var/nfs localhost(insecure,rw,sync,no_subtree_check)
6.3 Actualizar la tabla de file systems exportados por NFS:
sudo exportfs -ra
6.4 Para ver los file systems exportados por NFS:
sudo exportfs
6.5 Para activar la nueva configuración, se requiere reiniciar el servidor NFS:
sudo systemctl restart nfs-kernel-server
7. Ahora debemos abrir el puerto 2049 en el portal de Azure:
Intervalos de puertos de destino: 2049
Protocolo: TCP
Nombre: puerto_nfs
Instalación en el cliente
8. Para instalar NFS en el cliente se ejecutan los siguientes comandos:
sudo apt update
sudo apt install nfs-common
sudo apt install portmap
9. Crear el directorio de montaje en el cliente (punto de montaje):
sudo mkdir -p /nfs
10. Para crear un túnel SSH entre cliente y el servidor se ejecuta el siguiente comando:
ssh -fNv -L 3049:localhost:2049 ubuntu@40.84.237.35
En este ejemplo el puerto local 3049 del cliente se conecta al puerto 2049 del servidor. En este caso "ubuntu" es un usuario en el servidor y "40.84.237.35" es la dirección IP del servidor.
11. Montar el directorio remoto:
sudo mount -t nfs -o port=3049 localhost:/var/nfs /nfs
12. Para desmontar el directorio remoto /nfs:
sudo umount /nfs
Domain Name System (DNS)
La clase de hoy veremos otro ejemplo de espacio de nombres: el sistema de nombres de dominio (DNS: Domain Name System).
Un DNS es un espacio de nombres distribuido a gran escala, organizado jerárquicamente en tres capas.
Capa global
La capa global se compone de los nodos de más alto nivel, a saber, el nodo raíz y sus hijos. Todos los nodos en esta capa son nodos directorio.
Las etiquetas de los arcos son los diferentes tipos de dominio (com, org, edu, net, mx, etc.). Las tablas de directorio en la capa global casi nunca se modifican.
Capa de administración
La capa de administración se compone de nodos directorio que son administrados dentro de una misma organización.
Por ejemplo, un nodo podría corresponder al subdominio llamado “sun” y este tener un subdominio llamado “eng”, en este caso el dominio sería eng.sun.com
Las tablas de directorio de la capa de administración se modifican poco, debido a que generalmente representan unidades administrativas dentro de una organización.
Capa de dirección
La capa de dirección se compone de nodos que pueden modificarse con cierta frecuencia. Los nodos en esta capa representan servidores con el último subdominio
La siguiente figura muestra un ejemplo del espacio de nombres de un DNS. En esta figura se pueden ver partes del espacio de nombres llamadas zonas, las cuales se manejan mediante servidores de nombres por separado.
Fuente: Sistemas Distribuidos, Principios y Paradigmas, Andrew S. Tanenbaum, Pearson.
Resolución de nombre en un DNS
Cuando un usuario escribe una URL en un navegador web, se inicia un proceso de resolución de nombres para la URL, en primer lugar, se debe resolver el dominio al cual se va a conectar el navegador.
La resolución del dominio la realiza un solucionador de nombre dentro del sistema operativo que ejecuta el navegador.
Supongamos que el usuario escribe la siguiente URL en su navegador web:
ftp://ftp.cs.vu.nl/pub/globe/index.html
El nombre de ruta correspondiente sería el siguiente:
root:nl,vu,cs,ftp,pub,globe,index.html
Esto significa que el usuario requiere acceder al archivo “index.html” el cual se encuentra en el directorio “/pub/globe” en el servidor cuyo dominio es “ftp.cs.vu.nl”.
Para resolver la URL existen dos técnicas, la resolución iterativa y la resolución recursiva.
Resolución iterativa
La resolución iterativa del nombre de ruta anterior se podría realizar en tres iteraciones:
1) El solucionador de nombre del cliente se conecta a un solucionador de nombre root cuya dirección IP es conocida enviando la ruta <nl,vu,cs,ftp>. Este servidor resolverá el nombre hasta dónde le sea posible, en este caso solo puede resolver la etiqueta “nl”, entonces regresará al solucionador de nombre del cliente la dirección IP del solucionador de nombre nl y la ruta restante <vu,cs,ftp>.
2) El solucionador de nombre del cliente se conecta al solucionador de nombre nl enviando la ruta <vu,cs,ftp>. Este servidor solo puede resolver la etiqueta “vu”, entonces regresará al solucionador de nombre del cliente la dirección IP del solucionador de nombre vu y la ruta restante <cs,ftp>.
Resolución recursiva
Debido a que el solucionador de nombre del cliente suele estar lejos de los solucionadores de nombres, la resolución iterativa puede ser más costosa en términos de latencia de la comunicación.
Una alternativa a la resolución iterativa de nombres es el uso de la técnica de resolución recursiva.
1) En la resolución recursiva, el solucionador de nombre del cliente se comunica con el solucionador de nombre root enviando la ruta <nl,vu,cs,ftp>, este servidor solo puede resolver la etiqueta “nl” por tanto se comunica con el solucionador de nombre nl enviando el resto de la ruta <vu,cs,ftp>.
2) El solucionador de nombre nl sólo puede resolver la etiqueta “vu” por tanto se comunica con el solucionador de nombre vu enviando el resto de la ruta <cs,ftp>.
3) Finalmente, el solucionador de nombre vu resuelve las etiquetas “cs” y “ftp”, entonces regresará al solucionador de nombre nl la dirección IP del servidor ftp. El solucionador de nombre nl le envía la dirección IP al solucionador de nombre root, y este le enví la IP al solucionador de nombre del cliente.
Los datos son el principal activo de las empresas e instituciones.
Si bien es cierto que los sistemas informáticos son de gran importancia para automatizar los procesos en las empresas, los datos representan los objetos de negocio que tienen mayor persistencia en el tiempo, ya que ellos corresponden a lo que se sabe de los clientes, los productos, los insumos, los activos, los pasivos, las ventas, los procesos, los empleados, y muchos objetos de negocio más.
La replicación de los datos es una estrategia utilizada para mantener copias consistentes de los datos en diferentes locaciones, con el objetivo de tener la capacidad de recuperar la operación en caso de desastre.
¿Por qué replicar los datos?
Los datos se replican para satisfacer dos requerimientos no funcionales de los sistemas distribuidos: la confiabilidad y el rendimiento.
Replicar los datos en diferentes sistemas de archivos, aumenta la confiabilidad del sistema, ya que si una copia de los datos falla es posible seguir trabajando con otra copia de los datos.
Por ejemplo, en los DBMS se acostumbra configurar copias "espejo" de las tablas, de tal manera que cuándo se inserta, modifica o borra datos en una tabla, se realicen las mismas operaciones sobre una tabla "espejo", si la lectura de la tabla principal falla, entonces el DBMS automáticamente realiza la lectura sobre la copia "espejo", sin mayor intervención del sistema que accede a la base de datos.
La replicación de datos también mejora el rendimiento de un sistema distribuido que requiere escalar en tamaño y geografía.
La replicación de los datos permite acercar los datos al sistema, lo cual disminuye la latencia en el acceso. Por ejemplo, la replicación de datos que se modifican poco como es el caso de los catálogos de un sistema (clientes, productos, cuentas, etc.) permite acceder a éstos datos más rápido.
No obstante las ventajas que tiene la replicación de los datos, la principal dificultad es mantener la consistencia entre las copias. La consistencia de los datos significa que todas las copias deben tener los mismo datos.
Mantener la consistencia entre copias puede impactar el rendimiento del sistema, debido a que la actualización de las copias representa un costo en tiempo y recursos (CPU, red, almacenamiento, etc). Entonces será necesario evaluar el costo de mantener consistentes las copias y el beneficio del aumento del rendimiento que trae la replicación de los datos.
Modelos de consistencia
Un modelo de consistencia es un acuerdo entre los procesos que acceden un almacén de datos y el almacén de datos.
El acuerdo establece las reglas que deben obedecer los procesos cuando acceden el almacén de datos de manera que los procesos puedan tener una imagen consistente de los datos.
El almacén de datos puede ser una base de datos distribuida, un sistema de archivos distribuido o una combinación de ambos.
El principio en que se basa un modelo de consistencia es que si un proceso realiza una operación de lectura sobre elemento de datos, se espera leer el resultado de la última escritura sobre el mismo elemento de datos, independientemente de qué proceso realizó la escritura.
Consistencia secuencial
El modelo de consistencia secuencial fue propuesto por Lamport (1979), y dice que un almacén de datos es secuencialmente consistente si:
"El resultado de cualquier ejecución es el mismo que si las operaciones (de lectura y escritura) de todos los procesos efectuados sobre el almacén de datos se ejecutaran en algún orden secuencial y las operaciones de cada proceso individual aparecieran en esa secuencia en el orden especificado por su programa".
Esto significa que el almacén de datos debería "ver" las operaciones de lectura y escritura que realizar todos los procesos como si tratara de una secuencia de operaciones de lectura y escritura realizadas por un solo proceso.
La consistencia secuencial corresponde a operaciones de lectura y escritura ordenadas mediante la relación happen-before. Si A es un dato en el almacén de datos, entonces para cada par de operaciones write(A), read(A) se deberá cumplir write(A) happen-before read(A), es decir, la escritura de un dato debe preceder a la lectura del dato.
Consistencia de entrada
El modelo de consistencia secuencial propuesto por Lamport es un modelo de granularidad fina pensado originalmente para ser implementado en hardware para acceder localidades de memoria compartida en sistemas multiprocesadores, sin embargo , este modelo de consistencia resulta muy costoso para los sistemas distribuidos dónde los datos tienen granularidad gruesa, como son los registros, tablas, archivos, etc.
B.N. Bershad et al. (The Midway Distributed Shared Memory System, 1993) propuso un modelo de consistencia basado en la relación entre objetos de sincronización (locks) que protegen secciones criticas y los datos compartidos dentro de las secciones críticas.El modelo de consistencia de entrada utiliza objetos de sincronización exclusiva y no-exclusiva (compartida) para garantizar el orden en que se ejecutan las operaciones de lectura y escritura sobre un mismo elemento de datos.
Una sección crítica comienza con una operación de adquisición del lock y termina con la liberación de lock. A estas operaciones se les llama generalmente "lock" y "unlock".
Las reglas que se debe cumplir en este modelo de consistencia son:
1. Cuando un proceso ejecuta la operación "lock" ésta debe esperar a que se realicen todas las operaciones de escritura de los datos compartidos por el proceso.
2. Un proceso no puede adquirir un lock si algún otro proceso lo adquirió ya sea en forma exclusiva o compartida.
3. Si un proceso adquirió un lock en forma exclusiva, ningún otro proceso puede adquirir el lock en forma compartida.
Estas reglas garantizan que las lecturas de datos compartidos (que se realizan dentro de una sección crítica) obtendrán los datos actualizados por la última escritura, la cual también se debió realizar dentro de una sección crítica.
Como puede observarse, no importa el orden en que se realizan las lecturas y escrituras a los elementos de datos, lo que importa es el orden en que se realizan las operaciones "lock" y "unlock".
Para lograr la consistencia de los elementos de datos compartidos, los objetos de sincronización (locks) deberán implementarse en forma global, tal como se explicó en el tema de "Sincronización y coordinación".
Supongamos que tenemos dos threads t1 y t2, y cada thread ejecuta un ciclo dónde se incrementa la variable global n.
class A extends Thread
{
static long n;
public void run()
{
for (int i = 0; i < 100000; i++)
n++;
}
public static void main(String[] args) throws Exception
{
A t1 = new A();
A t2 = new A();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(n);
}
}
Al ejecutar varias veces el programa podemos ver que el valor final de n no es el mismo ¿por qué?
La instrucción n++ se compone de tres operaciones:
Debido a que los threads t1 y t2 ejecutan en paralelo, en una computadora con dos o más núcleos el thread t1 leerá y escribirá la variable n al mismo tiempo que el thread t2:
Dado que la lectura que realiza t1 no está ordenada con respecto a la escritura que hace t2 y que la lectura que realiza t2 no está ordenada con respecto a la escritura que hace t1, es posible que no se escriba algún incremento en la variable n, lo que produce un valor final menor a 200000.
Para resolver el problema se requiere que el programador identifique las secciones críticas y agregue las operaciones "lock" y "unlock" necesarias.
En este caso, la sección crítica es la instrucción n++, que es dónde se lee y escribe la variable n compartida por los dos threads.
Por lo tanto, es necesario ejecutar "lock" antes de n++ y ejecutar "unlock" después.
En java se utiliza la instrucción synchronized(objeto){ bloque-de-instrucciones } para definir un bloque de instrucciones como sección crítica controlada por el lock que contiene el objeto (recordemos que en Java todos los objetos incluyen un lock).
Entonces el código del programa queda de la siguiente manera:
class A extends Thread
{
static long n;
static Object obj = new Object();
public void run()
{
for (int i = 0; i < 100000; i++)
synchronized(obj)
{
n++;
}
}
public static void main(String[] args) throws Exception
{
A t1 = new A();
A t2 = new A();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(n);
}
}
Al ejecutar varias veces el programa anterior podemos ver que el valor final de la variable n siempre es 200000, debido a que ahora las operaciones de lectura y escritura se ejecutan en el orden correcto:
Memoria compartida distribuida
Un sistema de memoria compartida distribuida (DSM: Distributed Shared Memory) es una capa de software o hardware que implementa un área de memoria compartida a la que cada computador en un cluster tiene acceso además de su memoria local.
En una red de computadoras, cada computadora tiene acceso a su memoria local (RAM). Cada computadora realiza dos operaciones sobre la memoria local:
Un DSM requiere garantizar la consistencia de los datos, esto es, cada nodo deberá ver los mismos datos en la memoria compartida. Por tanto, la implementación de un DSM implica la utilización de un modelo de consistencia.
Para implementar un DSM podemos utilizar el modelo de consistencia de entrada, debido a que en este modelo de consistencia solo se debe ordenar las operaciones de bloqueo y desbloqueo, lo cual es más eficiente que ordenar todas las operaciones de escritura y lectura, como sería el caso del modelo de consistencia secuencial.
Entonces, el DSM deberá implementar cuatro operaciones: 1) escritura a memoria compartida, 2) lectura a memoria compartida, 3) bloqueo distribuido y 4) desbloqueo distribuido.
Ejemplo de ejecución distribuida utilizando DSM
Supongamos que se quiere incrementar una variable n utilizando cuatro nodos. Cada nodo ejecutará un ciclo de 100 iteraciones donde se lee una variable compartida n a un registro r, se incrementa el registro r y se escribe el registro r a la variable n. Al final, el nodo 0 desplegará el valor de la variable n.
Debido a que cada nodo lee y escribe una copia local de la variable n, el valor final de la variable n (en cada nodo) es 100, no obstante el resultado debería ser 400.
Entonces es necesario implementar un modelo de consistencia para garantizar que todos los nodos vean "la misma" variable n, en este caso, implementamos el modelo de consistencia de entrada:
Ahora se ha agregado las operaciones Lock y Unlock. De acuerdo al modelo de consistencia de entrada, cada vez que se ejecute la operación Lock, el nodo deberá recibir los cambios a las variables compartidas realizados por el nodo que ejecutó Unlock, en este caso la variable compartida es n.
Debido a que la sección crítica no puede ser ejecutada por más de una computadora, cada vez que un nodo ejecuta la operación Lock, solo la computadora que ejecutó la operación Unlock deberá enviará las escrituras realizadas a las variables compartidas, en este caso la variable n.
En esta actividad se implementará el modelo de consistencia de entrada en lenguaje Java.
Requerimientos funcionales
1. Se deberá implementar las operaciones lock y unlock utilizando exclusión mutua distribuida, utilizar el algoritmo de Ricart o el algoritmo de token-ring, los cuales implementamos en actividades anteriores.
2. Cada nodo creará un arreglo M de 10 enteros de 64 bits, el cual representará la memoria compartida.
3. Cada nodo creará un arreglo B de 10 booleanos los cuales indicarán qué elemento del arreglo de enteros fue modificado dentro de un bloque lock-unlock.
4. Cada nodo implementará las operaciones Read, Write, Lock y Unlock. La operación Read(n) leerá del arreglo M el elemento n, la operación Write(n,valor) escribirá el valor en el elemento n del arreglo M. Cuando se ejecute la operación Write(n,valor) se deberá asignar true al elemento n del arreglo B.
5. En la operación lock se deberá asignar false a todos los elementos del arreglo B.
6. Cuando se ejecute unlock, antes de desbloquear, se deberá enviar los cambios realizados en el arreglo M al resto de nodos.
7. Ejecutar el programa en cuatro nodos, en cada nodo se ejecutará:
7.1 Una barrera que espere que todos los nodos se encuentran en ejecución.
7.2 Ejecutar las siguientes instrucciones en un ciclo de 100 iteraciones:
Lock()
r=Read(0)
r++
Write(0,r)
Unlock()
7.3 Al final de las iteraciones el nodo 0 ejecutará los siguiente:
Lock()
System.out.println(Read(0))
Unlock();
En la clase anterior vimos que la replicación de los datos es una estrategia utilizada para aumentar la confiabilidad y rendimiento de los sistemas.
Replicar los datos permite mantener copias de los datos en diferentes lugares, si una copia falla entonces se puede acceder a otra copia.
La replicación de los datos tiene también un efecto positivo en el rendimiento de un sistema, debido a que es más rápido acceder a datos cercanos.
Para mantener consistentes las diferentes copias de los datos, es necesario implementar un modelo de consistencia. Mantener la consistencia de los datos suele ser complicado y costoso en tiempo de comunicación.
Respaldos incrementales
En la actualidad el cómputo en la nube nos permite realizar el aprovisionamiento dinámico de recursos de forma fácil (automática), rápida y a bajo costo.
En esta actividad vamos a realizar un ejercicio de replicación de un sistema completo, en este caso la replicación de una plataforma de servicios web con Tomcat y MySQL.
Como vimos en clase, para replicar un sistema, podemos crear una máquina virtual en la nube (réplica) que procese todas las peticiones que realizan los clientes, en paralelo al proceso de las mismas peticiones que realiza el sistema principal.
Vamos a utilizar el programa SimpleProxyServer.java el cual es un proxy escrito en Java, modificado por el profesor para que funcione como un administrador de tráfico.
Se deberá realizar lo siguiente:
1. Crear dos máquinas virtuales en la nube de Azure con Ubuntu 18, 1 GB de RAM y disco HDD estándar a partir de la imagen creada en la tarea 6.
2. Abrir el puerto 80 protocolo TCP en la máquina virtual 1.
3. Abrir el puerto 8080 protocolo TCP en la máquina virtual 2, ingresar en el campo "Origen" ("Source" si la pantalla está en inglés) la IP de la máquina virtual 1 (por seguridad, la máquina virtual 1 es la única computadora que podrá acceder la máquina virtual 2).
5. Utilizando el programa sftp enviar a la máquina virtual 1 el archivo: SimpleProxyServer.java
6. Compilar en la máquina virtual 1 el programa SimpleProxyServer.java
7. Iniciar Tomcat en las máquinas virtuales 1 y 2.
8. Ejecutar el máquina virtual 1 el proxy:
sudo java SimpleProxyServer ip-maquina-virtual-2 8080 80 8080 &
Donde IP-máquina-virtual-2 es la IP de la réplica, 8080 es el puerto abierto en la réplica (servidor Tomcat remoto), 80 es el puerto abierto en el sistema principal (proxy local) y 8080 es el puerto en la máquina virtual 1 dónde Tomcat recibe las peticiones (puerto de Tomcat local). Notar que no es necesario abrir el puerto 8080 en la máquina virtual 1, ya que el proxy y Tomcat se comunican localmente mediante loopback.
En este caso ejecutamos el proxy con "sudo" para que este pueda abrir el puerto 80 en la maquina virtual 1.
Probar el servicio web utilizando HTML-Javascript
9.1 Ingresar la siguiente URL en un navegador, notar que no es necesario ingresar el nombre del puerto, ya que se utiliza el puerto default 80:
http://ip-máquina-virtual-1/prueba.html
9.2 Dar clic en el botón “Alta usuario” para dar de alta un nuevo usuario. Capturar los campos y dar clic en el botón “Alta”.
9.3 Mostrar los registros insertados en la base de datos en la maquina virtual principal y la réplica (no desplegar el contenido del campo foto).
9.4 Dar clic en el botón “Consulta usuario” para consultar el usuario dado de alta en el paso 5. Capturar el email y dar clic en el botón “Consulta”,
9.5 Modificar algún dato del usuario y dar clic en el botón “Modifica”.
9.6 Mostrar los registros modificados en la base de datos en la maquina virtual principal y la réplica.
9.7 Consultar el usuario modificado, para verificar que la modificación se realizó.
9.8 Dar clic en el botón “Borra usuario” para borrar el usuario.
9.9 Mostrar los registros insertados en la base de datos en la maquina virtual principal y la réplica.
9.10 Capturar el email del usuario borrado y dar clic en el botón “Consulta”.
En el pasado el aprovisionamiento de recursos informáticos On-premise (en las instalaciones de la empresa) representaba el concurso de diferentes proveedores de bienes y servicios, como eran los representantes de ventas, ingenieros de pre-venta, fabricantes de los equipos, fabricante del sistema operativo, fabricante de la base de datos, agentes aduanales, transportistas, instaladores del site, proveedor de energía, proveedor de comunicaciones, instaladores del hardware, instaladores del software, entre otros.
Entonces, el aprovisionamiento de recursos informáticos era un proceso complejo y tardado, el cual culminaba con el sistema en producción.
Después había que re-aprovisionar cuándo crecían las necesidades de la empresa.
Cómputo en la nube
En 2006 aparece en la revista Wired el artículo The Information Factories de George Gilder que describe un nuevo modelo de arquitectura basado en una infraestructura de cómputo ofrecida como servicios virtuales a nivel masivo, a este nuevo modelo se le llamó cloud computing (cómputo en la nube).
El concepto clave en el cómputo en la nube es el "servicio", así, se ofrece infraestructura virtual y física como servicio (IaaS: Infrastructure as a Service), DBMS, plataformas de desarrollo y pruebas como servicio (PaaS: Platform as a Service), aplicaciones de software como servicio (SaaS: Software as a Service) y otros servicios con la terminación "as a Service", como Data as a Service (DaaS), Disaster Recovery as a Service (DRaaS), entre otros.
La elasticidad en la nube
Debido a que el cómputo en la nube está basado fundamentalmente en la virtualización de los recursos informáticos, este modelo de arquitectura ofrece una ventaja única, la posibilidad de hacer crecer y decrecer los recursos aprovisionados.
Supongamos un servicio de streaming bajo demanda, como es el caso de Netflix. En este tipo de servicio la demanda crece los fines de semana y decrece los días entre semana.
Si el proveedor del servicio no aprovisiona los recursos suficientes para atender la demanda del fin de semana, entonces muchos usuarios se quedarán sin servicio.
Por otra parte, si el proveedor del servicio aprovisiona los recursos necesarios para atender a sus usuarios el fin de semana, estos recursos estarán sub-utilizados los días entre semana, lo cual resulta en pérdidas económicas.
Sin lugar a dudas, el éxito que han alcanzado las empresas proveedoras de streaming bajo demanda, se debe a que su modelo de negocio está basado en la posibilidad que les ofrece la nube para crecer y decrecer los recursos aprovisionados, a esta característica de la nube se le llama elasticidad.
El cómputo elástico es la habilidad de hacer crecer y decrecer rápidamente la capacidad de cómputo (CPUs), la memoria y el almacenamiento para adaptarse a la demanda.
Para implementar el cómputo elástico se utilizan herramientas de monitoreo, las cuales aprovisionan y des-aprovisionan recursos conforme son necesarios, sin detener la operación.
Ver: What is elastic computing or cloud elasticity?
Nube pública, nube privada y nube híbrida
Se dice que la nube pública es un conjunto de servicios de TI ofrecidos mediante recursos (servidores, almacenamiento, red) propiedad de un proveedor de servicios en la nube, como es el caso de AWS, Azure, Softlayer, Oracle, Google, etc.Por otra parte, se entiende como nube privada aquellos servicios ofrecidos a partir de la virtualización de los recursos propiedad de la misma empresa.
En este mismo orden de ideas, la nube híbrida sería la mezcla de servicios de nube pública y servicios de nube privada.
Ver: ¿Qué es la nube pública, privada e híbrida?
Sin embargo, hay proveedores de nube que afirman que no existe tal cosa como "nube privada", ya que el tema de elasticidad se ve acotado en un equipo "privado" debido a la limitada escalabilidad.
En cambio, en la nube pública la escalabilidad es casi ilimitada, ya que si se agotan los recursos de un data center, sin ningún problema se puede escalar a otro data center, ya sea del mismo proveedor o de otros proveedores.
Una característica importante de la nube (pública) es "pagar por lo que se usa", esto significa que solo se paga por los recursos aprovisionados.
En el caso de un centro de cómputo tradicional (también es el caso de la "nube privada") la empresa paga por todo el equipo, lo use a toda su capacidad o no.
Consideremos la siguiente gráfica:
Escenario nube
Ahora supongamos que la empresa utiliza servicios en la nube. Entonces la inversión inicial es mínima, ya que la empresa contratará los servicios indispensables para operar. Conforme la empresa crece, la elasticidad de la nube permite adecuar el tamaño de la infraestructura informática de acuerdo a las necesidades.
Si la empresa decrece, también decrecen sus necesidades y el costo de la infraestructura informática. En la gráfica, las barras corresponden al costo de los servicios de nube. Podemos ver que este costo se ajusta a las necesidades de la empresa.
Podemos concluir que al utilizar los servicios de nube se optimiza la relación costo/beneficio, ya que solo pagamos lo que realmente usamos.
En esta actividad haremos un ejercicio de elasticidad en la nube, modificaremos el tamaño de un máquina virtual y el tamaño del disco de sistema operativo.
1. Crear una máquina virtual con Ubuntu.
2. Cambiar el tamaño de la máquina virtual.
Para cambiar el número CPUs virtuales y/o el tamaño de la memoria RAM de una máquina virtual:
3. Cambiar del tamaño del disco de sistema operativo.
Para cambiar el tamaño del disco de sistema operativo de una máquina virtual:
Notas
La clase de hoy vamos a ver el servicio de respaldos en la nube de Azure.
Iniciar un respaldo completo
Anteriormente vimos cómo habilitar un proceso de respaldo (backup job) para realizar un respaldo diario de una máquina virtual completa a cierta hora del día.
Para iniciar al momento el respaldo completo de la máquina virtual:
1. Seleccionar la máquina virtual en el portal de Azure. Seleccionar "Backup" en el menú de operaciones.
2. Seleccionar "Realizar copia de seguridad ahora" para crear el primer respaldo completo de la máquina virtual. Los subsecuentes respaldos automáticos serán incrementales.
3. Indicar la fecha de retención de la copia de seguridad o aceptar la fecha establecida en la política de respaldo utilizada (por omisión, 30 días).
4. Dar clic en el botón "Aceptar"
5. Dar clic en la campana de notificaciones para verificar que se haya iniciado el respaldo de la máquina virtual.
6. Para ver el progreso del respaldo seleccionar la opción "Ver todos los trabajos" en la página "Backup" de la máquina virtual. Seleccionar la opción "Actualizar" para refrescar la pantalla que muestra el estado del proceso de respaldo. El respaldo ha terminado cuando se despliega "Completada".
El tiempo que tarda el respaldo depende del número de procesadores virtuales, el tamaño de la memoria RAM y el tamaño del disco en la máquina virtual. El respaldo tardará más si la máquina virtual tiene poca memoria y pocos procesadores virtuales o el disco es grande.
Una vez terminado el respaldo en la página "Backup" de la máquina virtual aparecerá el punto de restauración creado.
Restaurar una máquina virtual
Para restaurar una máquina virtual completa:
1. Seleccionar la máquina virtual en el portal de Azure.En esta actividad vamos a encriptar un disco Ubuntu en la nube de Azure.
1. Verificar que el kernel soporte AES (Advanced Encryption Standard) a 256-bits:
cat /proc/crypto | grep aes
Si no aparece "aes", entonces ejecutar:
sudo modprobe aes
2. Instalar el módulo dm-crypt (si no está instalado):
sudo apt-get install dmsetup cryptsetup
3. Cargar el módulo dm-crypt:
sudo modprobe dm-crypt4. Verificar que el device-mapper haya reconocido el modulo dm-crypt y agregado crypt como destino disponible:
sudo dmsetup targets5. Crear bajo /mnt un punto para montar el device:
sudo mkdir /mnt/securedata6. Cambiar el propietario y grupo del directorio de montado (en este caso el propietario es el usuario "ubuntu" y el grupo es "ubuntu"), para que tenga acceso total a la partición encriptada:
sudo chown ubuntu /mnt/securedata
sudo chgrp ubuntu /mnt/securedata
Crear un disco en Azure
1. En la barra de búsqueda del portal de Azure ingresar: Discos
2. Seleccionar "+Crear"
3. Seleccionar el mismo grupo de recursos donde está la máquina virtual.
4. Ingresar el nombre del disco.
5. Seleccionar la misma región donde está la máquina virtual.
6. Seleccionar "Cambiar el amaño"
6.1 En el combo-box "SKU de disco" seleccionar el tipo de disco, en este caso "HDD estándar".
6.2 Seleccionar el tamaño del disco.
6.3 Dar clic en el botón "Aceptar".
7. Dar clic al botón "Review+create"
8. Dar clic al botón "Create"
9. Seleccionar la máquina virtual a la cual se adjuntará (atach) el disco.
10. En el menú Configuración seleccionar "Discos"
11. Seleccionar la opción "Adjuntar discos existentes"
12. En el combo-box "Nombre del disco" seleccionar el disco creado anteriormente.
13. Seleccionar la opción "Guardar"
14. Ingresar a la máquina virtual utilizando ssh.
15. Ejecutar el comando:
ls /dev/sd*Podemos ver que el nuevo disco se ha adjuntado (atached) como: /dev/sdc
16. Formatear el disco para encripción:
sudo cryptsetup -y luksFormat /dev/sdc17. Ingresar una contraseña para el disco (passphrase)
Nota: no debe almacenar la contraseña en la nube, es mejor ingresarlo manualmente
18. Verificar que el disco ha sido inicializado para encripción:
sudo cryptsetup luksDump /dev/sdc19. Inicialización de un device mapping para ser montado (aquí "securedata" es el nombre del device mapeado, /dev/mapper/securedata):
sudo cryptsetup luksOpen /dev/sdc securedata(ingresar la contraseña del disco)
20. Formatear el disco, en este caso con el tipo de filesystem ext4:
sudo mkfs.ext4 -m 0 /dev/mapper/securedata21. Montar el disco:
sudo mount /dev/mapper/securedata /mnt/securedata22. Para verificar que los datos efectivamente son encriptados en el disco:
sudo cryptsetup status securedata23. Para desmontar el disco:
sudo umount /mnt/securedata
sudo cryptsetup luksClose securedata
24. Para montar nuevamente el disco (cada vez que se inicie la máquina virtual):
sudo cryptsetup luksOpen /dev/sdc securedata
(ingresar la contraseña del disco)
sudo mount /dev/mapper/securedata /mnt/securedata
25. Para ver el disco montado ejecutar el siguiente comando:
dfLeer el disco crudo
El programa lee_disco.c permite leer directamente el disco para verificar que los datos se almacenan encriptados.
1. Instalar gcc:
sudo apt-get update
sudo apt-get install gcc
2. Capturar el siguiente programa:
// lee_disco.c// Carlos Pineda G. 2012
#include <stdio.h>
#include <ctype.h>
#include <errno.h>
#define LONG_BUFFER (4*1024)
char buffer[LONG_BUFFER];
void main ()
{
FILE *f = fopen("/dev/sdc","rb");
int i;
if (f == NULL)
{
fprintf(stderr,"Error al abrir el filesystem, errno=%i\n",errno);
return;
}
for(;;)
{
fread(buffer,LONG_BUFFER,1,f);
if (feof(f))
break;
for (i = 0; i < LONG_BUFFER; i++)
if (isprint(buffer[i]))
printf("%c",buffer[i]);
}
}
3. Compilar el programa:
gcc -o lee_disco lee_disco.c4. Ejecutar el programa:
sudo ./lee_discoPodemos ver que los datos en el disco /dev/sdc están encriptados.
5. Ahora modificamos el programa para leer el disco /dev/sda1 (disco de sistema operativo), entonces podemos ver que los datos en este disco no están encriptados.
En esta actividad vamos a restaurar una máquina virtual a partir de un vault (Almacén de Recovery Services).
Para restaurar una máquina virtual a partir de un punto de restauración en un vault (sin tener la máquina virtual respaldada) hacer lo siguiente:
1. Crear una red virtual
1.1 En la barra de búsqueda de Azure escribir: Redes virtuales
1.2 Seleccionar +Crear
1.3 Seleccionar el grupo de recursos donde se encuentra el vault.
1.4 Ingresar el nombre de la red virtual.
1.5 Seleccionar la misma región donde se encuentra el vault.
1.6 Dar clic en el botón "Review+create"
1.7 Dar clic en el botón "Create"
2. Crear una cuenta de almacenamiento
2.1 En la barra de búsqueda de Azure escribir: cuentas de almacenamiento
2.2 Dar clic en la opción +Crear
2.3 Seleccionar el grupo de recursos del vault.
2.4 Ingresar un nombre para la cuenta de almacenamiento (no debe existir en Azure).
2.5 Seleccionar la misma ubicación del vault.
2.6 En "Replicación" seleccionar "Almacenamiento con redundancia local (LRS)"
2.7 Dar clic en el botón "Revisar y crear".
2.8 Dar clic en el botón "Crear".
3. Restaurar la máquina virtual a partir del vault
3.1 En la barra de búsqueda de Azure escribir: centro de copias de seguridad
3.2 Seleccionar la opción "Restaurar".
3.3 En el campo "Tipo de origen de datos" seleccionar "Azure Virtual Machines".
3.4 Dar clic a la opción ""Seleccionar" en el campo "Intancia de copia de seguridad".
3.5 Seleccionar la instancia de copia de seguridad en la lista que aparece.
3.6 Dar clic al botón "Seleccionar".
3.7 Dar clic al botón "Continuar".
3.8 Dar clic a la opción "Seleccionar" en el campo "Punto de restauración".
3.9 Seleccionar el punto de restauración que interesa restaurar (ver la fecha y hora del respaldo que interesa).
3.10 Dar clic en el botón "Aceptar".
3.11 Dar clic en el botón "Restaurar".
Nota. Si el proceso de respaldo a restaurar se detuvo (ver la actividad anterior) y se borraron los datos (soft delete), hay que deshabilitar "soft delete" para poder desencadenar el proceso de restauración. Ver: Disabling soft delete using Azure portal. Después de deshabilitar el "soft delete" se deberá seleccionar la opción "Elementos de copia de seguridad" en el menú "Elementos protegidos" (a la izquierda de la pantalla). Seleccionar "Azure Virtual Machine", dar clic en los tres puntos a la derecha y seleccionar la opción "Recuperar".
3.12 En la pantalla del centro de copias de seguridad se puede ver el avance de la operación "Restaurar", esperar a que el proceso pase de "En curso" a "Completado".
3.13 Ahora ya podemos acceder a la nueva máquina virtual (restaurada) utilizando el usuario y contraseña de la máquina virtual respaldada.
Serverless es un servicio de cómputo sobre demanda al nivel de plataforma (PaaS), donde el proveedor de nube pone a disposición la infraestructura para la ejecución de código como respuesta a un requerimiento (petición).
Azure ofrece un servicio serverless llamado Function as a Service, FaaS o simplemente Azure Functions.
Azure Functions tiene muchas ventajas, entre otras:
Triggers
Un trigger (desencadenador) es el disparador de una función en Azure. Hay diferentes tipos de triggers, por ejemplo peticiones HTTP, Timers y Operaciones de datos.
En el caso de un trigger de tipo petición HTTP, el trigger invoca la función cuando recibe una petición.
Por otra parte, un trigger de tipo timer invoca la función cada vez que pasa un determinado intervalo de tiempo.
Una función sólo puede tener un trigger especificado.
Bindings
Los bindings permiten conectar una función a recursos de datos. Hay dos tipos de bindigs: bindings de entrada y bindings de salida.
Los bindings de entrada permiten a la función leer datos del recurso de datos, y los bindigns de salida permiten a la función escribir datos al recurso de datos.
Los triggers y los bindings se configuran en una archivo llamado function.json
Aplicación de funciones
Una aplicación de funciones es el contexto donde las funciones van a ejecutar, define una configuración común para las funciones dentro de la aplicación (lenguaje de programación, variables de entorno, claves de acceso, configuración de TLS/SSL, escalabilidad horizontal, etc.).
En los términos de servicios web podemos ver una aplicación de funciones como un servicio web. En estos mismos términos, una función sería un método web.
Ver: Serverless - BBVA
Ver el video:
Azure Functions puede integrarse con sistemas de control de versiones como GitHub y otros.
En esta actividad vamos a crear una función utilizando solamente el portal de Azure, lo cual es el método más simple.
Actualmente el portal de Azure solo soporta el desarrollo de funciones sobre Windows en los lenguajes C# script, Javascript y PowerShell.
El soporte para Linux y otros lenguajes requiere la instalación del ambiente de desarrollo en la computadora local. Ver: Supported languages in Azure Functions
Debido a que Azure Functions es un servicio a nivel de plataforma, la ejecución sobre Windows no tiene mayor importancia, excepto posiblemente, por un costo de ejecución ligeramente mayor, dado que Windows requiere algo más de memoria RAM que Linux.
Crear la aplicación de funciones
1. En la barra de búsqueda escribir: Aplicaciones de funciones
2. Seleccionar la opción Crear+
3. Crear un grupo de recursos o seleccionar un grupo de recursos existente.
4. Ingresar el nombre de la función (el nombre debe ser único en Azure). Notar que al nombre se le agrega el nombre de dominio "azurewebsites.net".
5. En el campo "Pila del entorno en tiempo de ejecución" seleccionar ".NET"
6. Seleccionar la región, por ejemplo East US
7. Dar clic en el botón "Siguiente: Hospedaje"
8. Seleccionar sistema operativo Windows
9. En el campo "Tipo de plan" seleccionar "Consumo (sin servidor)", esta opción utiliza serverless escalable a un costo mínimo.
10. Dar clic en el botón "Revisar y Crear"
11. Dar clic en el botón "Crear"
12. Dar clic en el ícono de notificaciones (la campana en la parte superior de la pantalla) para revisar el avance del proceso de creación de la función.
13. Dar clic en el botón "Ir al recurso"
Crear una función
14. Seleccionar la opción "Funciones" en el menú a la izquierda de la pantalla.
Notas.
15. Seleccionar Crear+
16. En el campo "Entorno de desarrollo" seleccionar "Desarrollar en el portal".
17. Seleccionar la plantilla "HTTP trigger".
18. Dar clic en el botón "Crear". Por default se creó el trigger HttpTrigger1.
19. Seleccionar la opción "Código y prueba". Se muestra el código en lenguaje C#. En el combo box se puede seleccionar el archivo a visualizar (run.csx, function.json o readme.md)
20. Seleccionar la opción "Obtener la dirección URL de la función". Dar clic en el ícono de pegar.
21. Abrir una ventana en un navegador web.
22. Pegar la URL copiada anteriormente y dar enter.
23. Ahora pegar la siguiente cadena de caracteres a la URL en el navegador y dar enter:
&name=hugo
24. Cambiar la función por la siguiente:
public static async Task<IActionResult> Run(HttpRequest req, ILogger log){log.LogInformation("C# HTTP trigger function processed a request.");// obtiene los parámetros que pasan en la URLstring name = req.Query["name"];string edad = req.Query["edad"];// obtiene los parámetros que pasan en el "body"string requestBody = await new StreamReader(req.Body).ReadToEndAsync();dynamic data = JsonConvert.DeserializeObject(requestBody);name = name ?? data.name;edad = edad ?? data.edad;string responseMessage = string.IsNullOrEmpty(name) || string.IsNullOrEmpty(edad)? "Se debe pasar los parámetros name y edad": "Hola " + name + " tienes " + edad + " años";return new OkObjectResult(responseMessage);}
25. Seleccionar la opción "Guardar". Si no hay errores de compilación se muestra el letrero "Compilation succeeded".
26. Pegar la siguiente cadena de caracteres a la URL en el navegador y dar enter:
&name=hugo&edad=20
27. Ahora vamos a probar la función directamente en el portal.
27.1 Seleccionar la opción "Probar/ejecutar".
27.2 En el campo "Método HTTP" seleccionar GET.
27.3 En el campo "cuerpo" ingresar un objeto JSON vacío: { }
27.4 Dar clic en el botón "Ejecutar".
27.5 Seleccionar la opción "+Agregar parámetro". Ingresar el nombre y valor para los parámetros name y edad.
27.6 Dar clic en el botón "Ejecutar".
27.7 En el campo "Método HTTP" seleccionar POST.
27.8 Borrar los parámetros que se agregaron anteriormente.
27.9 En el campo "cuerpo" ingresar un objeto JSON con los parámetros name y edad: {"name":"hugo","edad":20}
27.10 Dar clic en el botón "Ejecutar".
Para una introducción al desarrollo de funciones utilizando C# script ver: Azure Functions C# script (.csx) developer reference
En clases anteriores vimos servicios de la nube al nivel de infraestructura (IaaS: Infrastructure as a Service), como son la creación de máquinas virtuales, la creación de imágenes de máquinas virtuales, el respaldo de máquinas virtuales (Azure Backup) y el balance de carga (Azure Load Balancer).
La clase de hoy veremos el servicio administrado de base de datos MySQL, el cual ofrece Azure al nivel de plataforma (PaaS: Platform as a Service), .
Platform as a Service
Cuándo una empresa instala sus sistemas en la nube requiere así mismo tener acceso a un DBMS.
Uno de los más populares manejadores de bases de datos en la actualidad es MySQL (usado por empresas de clase mundial como Google, YouTube, Facebook, Twitter, PayPal, etc.), el cual puede utilizarse en su versión gratuita (Community Server) o en su versión Enterprise Edition.
Como hemos visto en clases previas, es fácil instalar MySQL sobre una máquina virtual, crear una base de datos y acceder a ella utilizando el monitor de mysql o un programa escrito en algún lenguaje como Java, C#, Python, Node.js, entre otros.
Sin embargo, en un entorno empresarial será necesario llevar a cabo la administración de MySQL, lo cual incluye la instalación, monitoreo, respaldo de las bases de datos, recuperación ante desastres, espejo de discos, etc.
Entonces la empresa tiene dos opciones, la primera es contratar el personal que se encargue de la administración del DBMS, y la segunda contratar el servicio completo en la nube. Este servicio de DBMS completamente administrado es un ejemplo de plataforma como servicio (PaaS).
Azure Database for MySQL
Azure ofrece MySQL Community Server completamente administrado como servicio. Este servicio de nube permite a la empresa delegar la administración de la base de datos y contar con alta disponibilidad (99.99%) y escalabilidad dinámica, así como el acceso remoto a MySQL mediante conexión segura.
En esta actividad vamos a crear una instancia de MySQL al nivel de PaaS en la nube de Azure.
Crear de un servidor en Azure Database for MySQL
Para crear una instancia de MySQL:
1. Ingresar al portal de Azure.
2. En la barra de búsqueda escribir: Servidores de Azure Database for MySQL
3. Dar clic en "+Crear"
4. Dar clic en el botón "Crear" en el recuadro "Un solo servidor".
5. Seleccionar un grupo de recursos existente o crear uno nuevo.
6. Ingresar el nombre del servidor, por ejemplo: prueba-mysql
7. Seleccionar "Configurar servidor"
8. En la pantalla "Plan de tarifa":
8.1 Seleccionar "Basico"
8.2 Reducir el número de CPUs virtuales (vCore) a 1.
8.3 Reducir el almacenamiento a 5 GB.
En la parte derecha de la pantalla se podrá ver el resumen de precios.
9. Una vez configurado el servidor dar clic en el botón "Aceptar"
10. Ingresar el nombre del usuario administrador de MysQL, por ejemplo: administrador
11. Ingresar la contraseña del usuario administrador.
12. Dar clic en el botón "Revisar y crear"
Podemos ver el costo estimado al mes, en este caso 30.38 USD por 1 CPU virtual, 5 MB de almacenamiento, 7 días de retención de copia de seguridad, redundancia local de copia de seguridad y crecimiento automático de almacenamiento habilitado.
13. Dar clic en el botón "Crear"
14. Dar clic en la campana de notificaciones para revisar la implementación en curso.
15. Cuándo termine la implementación del servidor, dar clic en el botón "Ir al recurso"
Conexión al servidor MySQL
Ahora vamos a conectarnos al servidor de MySQL recién instalado utilizando el monitor de MySQL:
1. En la parte izquierda de la pantalla seleccionar "Seguridad de la conexión"
2. Ingresar en "Nombre de la regla de firewall" el nombre de la regla, por ejemplo: regla1
4. Ahora vamos a configurar el firewall del servidor:
4.1 Para que la computadora actual pueda conectarse al servidor (p.e. para ejecutar el monitor de mysql), seleccionar la opción "+ Agregar dirección IP del cliente actual".
4.2 Para que otra computadora se pueda conectar al servidor, ingresar en los campos "IP inicial" y en "IP final" la IP de la computadora.
4. 3 Para que cualquier computadora se pueda conectar al servidor, ingresar 0.0.0.0 como IP inicial y 255.255.255.255 como IP final, pero esto no es recomendable por razones de seguridad.
4.4 Para que todos los recursos de Azure (incluso los que no están en la misma suscripción) tengan acceso al servidor (p.e. Azure Functions), seleccionar "Si" en la opción "Permitir el acceso a servicios de Azure".
5. Verificar en "Configuración SSL" que SSL esté habilitado.
6. Dar clic en el botón "Guardar"
7. En el panel (seleccionar la opción "Información general" en el menú de la izquierda de la pantalla) podemos ver el nombre del dominio del servidor (en este caso el servidor se llama prueba-mysql):
prueba-mysql.mysql.database.azure.com
8. Ahora podemos conectarnos al servidor de MySQL ejecutando el monitor de MySQL, en este caso se ejecuta en una computadora con Ubuntu en la cual se ha instalado previamente MySQL (en esta caso el usuario es "administrador"):
mysql -u administrador@prueba-mysql -p -h prueba-mysql.mysql.database.azure.com --ssl-mode REQUIRED
Como puede verse, la conexión con MySQL se realiza mediante SSL.
create database servicio_web;
use servicio_web;
create table usuarios
(
id_usuario integer auto_increment primary key,
email varchar(100) not null,
nombre varchar(100) not null,
apellido_paterno varchar(100) not null,
apellido_materno varchar(100),
fecha_nacimiento datetime not null,
telefono varchar(20),
genero char(1)
);
create table fotos_usuarios
(
id_foto integer auto_increment primary key,
foto longblob,
id_usuario integer not null
);
alter table fotos_usuarios add foreign key (id_usuario) references usuarios(id_usuario);
create unique index usuarios_1 on usuarios(email);
create user hugo identified by 'contraseña-del-usuario-hugo';
grant all on servicio_web.* to hugo;
La clase de hoy vamos a ver el tema de balance de carga en la nube.
El balance de carga es la distribución equilibrada de carga entre un grupo de servidores (p.e. servidores web) o recursos en el back-end (p.e. unidades de almacenamiento).
La carga es el tráfico de red entrante a un recurso, por ejemplo las peticiones a un servidor, o las lecturas/escrituras a una unidad de almacenamiento.
Azure Load Balancer
El balanceador de carga en Azure opera en la capa de transporte (nivel 4) del modelo OSI, por tanto soporta los protocolos TCP y UDP.
El balanceador de carga utiliza un algoritmo de distribución haciendo el hash de cinco elementos: la IP de origen, el puerto de origen, la IP de destino, el puerto de destino y el tipo de protocolo
En Azure se puede crear dos tipos de balanceadores de carga:
Balanceador de carga público
Un balanceador de carga público mapea la dirección IP pública (IP de front-end) y el puerto (la IP pública y el puerto conforman un endpoint de Internet) a una dirección IP privada y puerto de una máquina virtual.
Utilizando reglas en el balanceador de carga, es posible distribuir la carga por tipo de tráfico (HTTP, HTTPS, FTP, SMTP, SSH, RDP, MySQL, SQLServer, etc).
Balanceador de carga interno
Un balanceador de carga interno distribuye el tráfico entre los recursos que se encuentran dentro de una red virtual.
Este tipo de balanceador de carga se utiliza para equilibrar la carga de las máquinas virtuales que ejecutan procesos de negocio que no son visibles al usuario externo.
La IP de un balanceador de carga interno nunca se expone como endpoint de Internet.
En un escenario de nube híbrida, es posible conectar servidores on-premise a un balanceador de carga interno.
Escalamiento mediante balance de carga
El balance de carga permite escalar las aplicaciones e implementar servicios con alta disponibilidad dentro de una zona y a través de diferentes zonas.
Agregando máquinas virtuales al balanceador de carga los sistemas incrementan su capacidad de atender peticiones, así mismo, se evita la interrupción del servicio cuándo uno de los servidores falla.
Costo del balance de carga en Azure
En Azure se puede configurar reglas de equilibrio de carga y reglas NAT. Las reglas NAT son utilizadas para mapear el tráfico entre las direcciones IP públicas y privadas.
Las reglas de equilibrio de carga se cobran por número de reglas por hora. Además se cobra la cantidad de datos procesados de entrada y de salida, independientemente de las reglas.
Azure ofrece dos versiones de balanceadores de carga: Basic y Standard. Ver: Azure Load Balancer SKUs
En el caso de Standard Load Balancer no se cobra por hora si no hay reglas configuradas. Las reglas NAT son gratuitas. Ver: Precios de Load Balancer
Ver las páginas:
Regions and Availability Zones in Azure
Ver el video:
Nota. Las máquinas virtuales que se agregarán al balanceador de carga deben estar en el mismo conjunto de disponibilidad. El conjunto de disponibilidad se deberá asignar al momento de crear la máquina virtual de la siguiente manera:
1. En el campo "Opciones de disponibilidad" seleccionar "Conjunto de disponibilidad".
2. En el campo "Conjunto de disponibilidad" seleccionar un conjunto de disponibilidad existente o bien, crear uno nuevo. Si se crea un nuevo conjunto de disponibilidad ingresar el nombre y dar clic en el botón "Aceptar". Las máquinas que se agregarán al balanceador de carga deben estar en el mismo conjunto de disponibilidad.
Creación de un balanceador de carga en Azure
1. Ingresar al portal de Azure.
2. En la barra de búsqueda ingresar: Equilibradores de carga
3. Seleccionar la opción "+Crear".
4. Seleccionar el grupo de recursos o bien, crear un nuevo grupo de recursos.
5. Ingresar un nombre para la instancia del balanceador de carga.
6. Seleccionar la región.
7. Seleccionar el tipo de balanceador: Público
8. Seleccionar el SKU: Básico
9. Seleccionar el Nivel: Regional
10. Seleccionar la opción "+Agregar una configuración de IP de front-end".11. Ingresar el nombre de la dirección IP pública.
12. En el campo "versión de IP" seleccionar "IPv4".
13. En el campo "Dirección de IP pública" seleccionar "Crear"
14. Ingresar el nombre de la IP pública (puede ser el mismo que se ingresó en el paso 11).
15. En el campo "Asignación" seleccionar "Dinámica".
16. Dar clic en el botón "Aceptar".
17. Dar clic en el botón "Agregar".
18. Dar clic en el botón "Revisar y crear".
19. Dar clic en el botón "Crear".
Configuración del balanceador de carga
1. En el inicio del portal de Azure seleccionar "Todos los recursos".
2. Seleccionar el balanceador (equilibrador) de carga a configurar.
Podemos ver la IP pública creada para el balanceador de carga.
3. Para agregar máquinas virtuales al balanceador de carga seleccionar la opción "Grupos de back-end" en la sección "Configuración" del menú que aparece a la izquierda de la pantalla.
4. Seleccionar la opción "+Agregar".
5. Ingresar un nombre para el grupo de recursos donde están las máquinas virtuales.
6. Seleccionar la red virtual.
7. En el campo "Asociado a" seleccionar "Maquinas virtuales".
8. Seleccionar la versión de IP: IPv4
9. Dar clic al botón "+Agregar" para agregar una máquina virtual al grupo back-end.
Nota importante. Las máquinas virtuales no deben tener IP pública y deben estar en la misma ubicación y red virtual que el balanceador de carga. Al crear cada máquina virtual seleccionar "Ninguno" en el campo "IP Pública" en la pestaña "Redes".
© Carlos Pineda Guerrero, 2021.